Commit d880633f authored by Thong Kuah's avatar Thong Kuah

Merge branch 'feat/x509-signed-commits' into 'master'

feat: x509 signed commits using openssl

See merge request gitlab-org/gitlab!17773
parents da4917c9 752c3e4d
......@@ -321,6 +321,16 @@
}
}
.gpg-popover-certificate-details {
ul {
padding-left: $gl-padding;
}
li.unstyled {
list-style-type: none;
}
}
.gpg-popover-status {
display: flex;
align-items: center;
......
......@@ -25,7 +25,7 @@ class Commit
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
attr_accessor :redacted_full_title_html
attr_reader :gpg_commit, :container
attr_reader :container
delegate :repository, to: :container
delegate :project, to: :repository, allow_nil: true
......@@ -123,7 +123,6 @@ class Commit
@raw = raw_commit
@container = container
@gpg_commit = Gitlab::Gpg::Commit.new(self) if container
end
delegate \
......@@ -320,13 +319,34 @@ class Commit
)
end
def signature
return @signature if defined?(@signature)
def has_signature?
signature_type && signature_type != :NONE
end
def raw_signature_type
strong_memoize(:raw_signature_type) do
next unless @raw.instance_of?(Gitlab::Git::Commit)
@raw.raw_commit.signature_type if defined? @raw.raw_commit.signature_type
end
end
@signature = gpg_commit.signature
def signature_type
@signature_type ||= raw_signature_type || :NONE
end
delegate :has_signature?, to: :gpg_commit
def signature
strong_memoize(:signature) do
case signature_type
when :PGP
Gitlab::Gpg::Commit.new(self).signature
when :X509
Gitlab::X509::Commit.new(self).signature
else
nil
end
end
end
def revert_branch_name
"revert-#{short_id}"
......
# frozen_string_literal: true
module X509SerialNumberAttribute
extend ActiveSupport::Concern
class_methods do
def x509_serial_number_attribute(name)
return if ENV['STATIC_VERIFICATION']
validate_binary_column_exists!(name) unless Rails.env.production?
attribute(name, Gitlab::Database::X509SerialNumberAttribute.new)
end
# This only gets executed in non-production environments as an additional check to ensure
# the column is the correct type. In production it should behave like any other attribute.
# See https://gitlab.com/gitlab-org/gitlab/merge_requests/5502 for more discussion
def validate_binary_column_exists!(name)
return unless database_exists?
unless table_exists?
warn "WARNING: x509_serial_number_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
return
end
column = columns.find { |c| c.name == name.to_s }
unless column
warn "WARNING: x509_serial_number_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations"
return
end
unless column.type == :binary
raise ArgumentError.new("x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary")
end
rescue => error
Gitlab::AppLogger.error "X509SerialNumberAttribute initialization: #{error.message}"
raise
end
def database_exists?
Gitlab::Database.exists?
end
end
end
# frozen_string_literal: true
class X509Certificate < ApplicationRecord
include X509SerialNumberAttribute
x509_serial_number_attribute :serial_number
enum certificate_status: {
good: 0,
revoked: 1
}
belongs_to :x509_issuer, class_name: 'X509Issuer', foreign_key: 'x509_issuer_id', optional: false
has_many :x509_commit_signatures, inverse_of: 'x509_certificate'
# rfc 5280 - 4.2.1.2 Subject Key Identifier
validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ }
# rfc 5280 - 4.1.2.6 Subject
validates :subject, presence: true
# rfc 5280 - 4.1.2.6 Subject (subjectAltName contains the email address)
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
# rfc 5280 - 4.1.2.2 Serial number
validates :serial_number, presence: true, numericality: { only_integer: true }
validates :x509_issuer_id, presence: true
def self.safe_create!(attributes)
create_with(attributes)
.safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier])
end
end
# frozen_string_literal: true
class X509CommitSignature < ApplicationRecord
include ShaAttribute
sha_attribute :commit_sha
enum verification_status: {
unverified: 0,
verified: 1
}
belongs_to :project, class_name: 'Project', foreign_key: 'project_id', optional: false
belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false
validates :commit_sha, presence: true
validates :project_id, presence: true
validates :x509_certificate_id, presence: true
scope :by_commit_sha, ->(shas) { where(commit_sha: shas) }
def self.safe_create!(attributes)
create_with(attributes)
.safe_find_or_create_by!(commit_sha: attributes[:commit_sha])
end
# Find commits that are lacking a signature in the database at present
def self.unsigned_commit_shas(commit_shas)
return [] if commit_shas.empty?
signed = by_commit_sha(commit_shas).pluck(:commit_sha)
commit_shas - signed
end
def commit
project.commit(commit_sha)
end
def x509_commit
return unless commit
Gitlab::X509::Commit.new(commit)
end
end
# frozen_string_literal: true
class X509Issuer < ApplicationRecord
has_many :x509_certificates, inverse_of: 'x509_issuer'
# rfc 5280 - 4.2.1.1 Authority Key Identifier
validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ }
# rfc 5280 - 4.1.2.4 Issuer
validates :subject, presence: true
# rfc 5280 - 4.2.1.14 CRL Distribution Points
# cRLDistributionPoints extension using URI:http
validates :crl_url, presence: true, public_url: true
def self.safe_create!(attributes)
create_with(attributes)
.safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier])
end
end
......@@ -6,7 +6,7 @@ module Git
execute_branch_hooks
super.tap do
enqueue_update_gpg_signatures
enqueue_update_signatures
end
end
......@@ -103,14 +103,22 @@ module Git
end
end
def enqueue_update_gpg_signatures
unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha))
def unsigned_x509_shas(commits)
X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
end
def unsigned_gpg_shas(commits)
GpgSignature.unsigned_commit_shas(commits.map(&:sha))
end
def enqueue_update_signatures
unsigned = unsigned_x509_shas(commits) & unsigned_gpg_shas(commits)
return if unsigned.empty?
signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
return if signable.empty?
CreateGpgSignatureWorker.perform_async(signable, project.id)
CreateCommitSignatureWorker.perform_async(signable, project.id)
end
# It's not sufficient to just check for a blank SHA as it's possible for the
......
- if signature
= render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
- uri = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}"
= render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature }
......@@ -17,12 +17,18 @@
- content = capture do
- if show_user
.clearfix
= render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
- uri_signature_badge_user = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}signature_badge_user"
= render partial: "#{uri_signature_badge_user}", locals: { signature: signature }
= _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
- if signature.instance_of?(X509CommitSignature)
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
= link_to(_('Learn more about x509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link')
- else
= _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
.gpg-popover-certificate-details
%strong= _('Certificate Subject')
%ul
- signature.x509_certificate.subject.split(",").each do |i|
- if i.start_with?("CN", "O")
%li= i
%li= _('Subject Key Identifier:')
%li.unstyled= signature.x509_certificate.subject_key_identifier.gsub(":", " ")
.gpg-popover-certificate-details
%strong= _('Certificate Issuer')
%ul
- signature.x509_certificate.x509_issuer.subject.split(",").each do |i|
- if i.start_with?("CN", "OU", "O")
%li= i
%li= _('Subject Key Identifier:')
%li.unstyled= signature.x509_certificate.x509_issuer.subject_key_identifier.gsub(":", " ")
- user = signature.commit.committer
- user_email = signature.x509_certificate.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_email: user_email, size: 32)
%div
%strong= user_email
- title = capture do
= _('This commit was signed with an <strong>unverified</strong> signature.').html_safe
- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true }
= 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.').html_safe
- locals = { signature: signature, title: title, label: _('Verified'), css_class: 'valid', icon: 'status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
......@@ -699,14 +699,14 @@
:latency_sensitive: true
:resource_boundary: :unknown
:weight: 2
- :name: create_evidence
:feature_category: :release_governance
- :name: create_commit_signature
:feature_category: :source_code_management
:has_external_dependencies:
:latency_sensitive:
:resource_boundary: :unknown
:weight: 2
- :name: create_gpg_signature
:feature_category: :source_code_management
- :name: create_evidence
:feature_category: :release_governance
:has_external_dependencies:
:latency_sensitive:
:resource_boundary: :unknown
......
# frozen_string_literal: true
class CreateGpgSignatureWorker
class CreateCommitSignatureWorker
include ApplicationWorker
feature_category :source_code_management
......@@ -23,7 +23,12 @@ class CreateGpgSignatureWorker
# This calculates and caches the signature in the database
commits.each do |commit|
Gitlab::Gpg::Commit.new(commit).signature
case commit.signature_type
when :PGP
Gitlab::Gpg::Commit.new(commit).signature
when :X509
Gitlab::X509::Commit.new(commit).signature
end
rescue => e
Rails.logger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
end
......
---
title: x509 signed commits using openssl
merge_request: 17773
author: Roger Meier
type: added
......@@ -42,12 +42,12 @@
- 2
- - container_repository
- 1
- - create_commit_signature
- 2
- - create_evidence
- 2
- - create_github_webhook
- 2
- - create_gpg_signature
- 2
- - create_note_diff_file
- 1
- - cronjob
......
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateX509Signatures < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :x509_issuers do |t|
t.timestamps_with_timezone null: false
t.string :subject_key_identifier, index: true, null: false, unique: true, limit: 255
t.string :subject, null: false, limit: 255
t.string :crl_url, null: false, limit: 255
end
create_table :x509_certificates do |t|
t.timestamps_with_timezone null: false
t.string :subject_key_identifier, index: true, null: false, unique: true, limit: 255
t.string :subject, null: false, limit: 255
t.string :email, null: false, limit: 255
t.binary :serial_number, null: false
t.integer :certificate_status, limit: 2, default: 0, null: false
t.references :x509_issuer, index: true, null: false, foreign_key: { on_delete: :cascade }
end
create_table :x509_commit_signatures do |t|
t.timestamps_with_timezone null: false
t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
t.references :x509_certificate, index: true, null: false, foreign_key: { on_delete: :cascade }
t.binary :commit_sha, index: true, null: false
t.integer :verification_status, limit: 2, default: 0, null: false
end
end
end
# frozen_string_literal: true
class MigrateCreateCommitSignatureWorkerSidekiqQueue < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'create_gpg_signature', to: 'create_commit_signature'
end
def down
sidekiq_queue_migrate 'create_commit_signature', to: 'create_gpg_signature'
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_02_05_143231) do
ActiveRecord::Schema.define(version: 2020_02_06_091544) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
......@@ -4482,6 +4482,40 @@ ActiveRecord::Schema.define(version: 2020_02_05_143231) do
t.index ["type"], name: "index_web_hooks_on_type"
end
create_table "x509_certificates", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "subject_key_identifier", limit: 255, null: false
t.string "subject", limit: 255, null: false
t.string "email", limit: 255, null: false
t.binary "serial_number", null: false
t.integer "certificate_status", limit: 2, default: 0, null: false
t.bigint "x509_issuer_id", null: false
t.index ["subject_key_identifier"], name: "index_x509_certificates_on_subject_key_identifier"
t.index ["x509_issuer_id"], name: "index_x509_certificates_on_x509_issuer_id"
end
create_table "x509_commit_signatures", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.bigint "project_id", null: false
t.bigint "x509_certificate_id", null: false
t.binary "commit_sha", null: false
t.integer "verification_status", limit: 2, default: 0, null: false
t.index ["commit_sha"], name: "index_x509_commit_signatures_on_commit_sha"
t.index ["project_id"], name: "index_x509_commit_signatures_on_project_id"
t.index ["x509_certificate_id"], name: "index_x509_commit_signatures_on_x509_certificate_id"
end
create_table "x509_issuers", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "subject_key_identifier", limit: 255, null: false
t.string "subject", limit: 255, null: false
t.string "crl_url", limit: 255, null: false
t.index ["subject_key_identifier"], name: "index_x509_issuers_on_subject_key_identifier"
end
create_table "zoom_meetings", force: :cascade do |t|
t.bigint "project_id", null: false
t.bigint "issue_id", null: false
......@@ -4973,6 +5007,9 @@ ActiveRecord::Schema.define(version: 2020_02_05_143231) do
add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
add_foreign_key "x509_certificates", "x509_issuers", on_delete: :cascade
add_foreign_key "x509_commit_signatures", "projects", on_delete: :cascade
add_foreign_key "x509_commit_signatures", "x509_certificates", on_delete: :cascade
add_foreign_key "zoom_meetings", "issues", on_delete: :cascade
add_foreign_key "zoom_meetings", "projects", on_delete: :cascade
end
---
type: concepts, howto
---
# Signing commits with x509
[x509](https://en.wikipedia.org/wiki/X.509) is a standard format for public key
certificates issued by a public or private Public Key Infrastructure (PKI).
Personal x509 certificates are used for authentication or signing purposes
such as SMIME, but beside that, Git supports signing of commits and tags
with x509 certificates in a similar way as with [GPG](../gpg_signed_commits/index.md).
The main difference is the trust anchor which is the PKI for x509 certificates
instead of a web of trust with GPG.
## How GitLab handles x509
GitLab uses its own certificate store and therefore defines the trust chain.
For a commit to be *verified* by GitLab:
- The signing certificate email must match a verified email address used by the committer in GitLab.
- The Certificate Authority has to be trusted by the GitLab instance, see also
[Omnibus install custom public certificates](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
- The signing time has to be within the time range of the [certificate validity](https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.5)
which is usually up to three years.
- The signing time is equal or later then commit time.
NOTE: **Note:** There is no certificate revocation list check in place at the moment.
## Obtaining an x509 key pair
If your organization has Public Key Infrastructure (PKI), that PKI will provide
an S/MIME key.
If you do not have an S/MIME key pair from a PKI, you can either create your
own self-signed one, or purchase one. MozillaZine keeps a nice collection
of [S/MIME-capable signing authorities](http://kb.mozillazine.org/Getting_an_SMIME_certificate)
and some of them generate keys for free.
## Associating your x509 certificate with Git
To take advantage of X509 signing, you will need Git 2.19.0 or later. You can
check your Git version with:
```sh
git --version
```
If you have the correct version, you can proceed to configure Git.
### Linux
Configure Git to use your key for signing:
```sh
signingkey = $( gpgsm --list-secret-keys | egrep '(key usage|ID)' | grep -B 1 digitalSignature | awk '/ID/ {print $2}' )
git config --global user.signingkey $signingkey
git config --global gpg.format x509
```
### Windows and MacOS
Install [smimesign](https://github.com/github/smimesign) by downloading the
installer or via `brew install smimesign` on MacOS.
Get the ID of your certificate with `smimesign --list-keys` and set your
signingkey `git config --global user.signingkey ID`, then configure x509:
```sh
git config --global gpg.x509.program smimesign
git config --global gpg.format x509
```
## Signing commits
After you have [associated your x509 certificate with Git](#associating-your-x509-certificate-with-git) you
can start signing your commits:
1. Commit like you used to, the only difference is the addition of the `-S` flag:
```sh
git commit -S -m "feat: x509 signed commits"
```
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:
```sh
git config --global commit.gpgsign true
```
## Verifying commits
To verify that a commit is signed, you can use the `--show-signature` flag:
```sh
git log --show-signature
```
# frozen_string_literal: true
module Gitlab
module Database
# Class for casting binary data to int.
#
# Using X509SerialNumberAttribute allows you to store X509 certificate
# serial number values as binary while still using integer to access them.
# rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum), could be:
# - 1461501637330902918203684832716283019655932542975
# - 0xffffffffffffffffffffffffffffffffffffffff
class X509SerialNumberAttribute < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea
PACK_FORMAT = 'H*'
def deserialize(value)
value = super(value)
value ? value.unpack1(PACK_FORMAT).to_i : nil
end
def serialize(value)
arg = value ? [value.to_s].pack(PACK_FORMAT) : nil
super(arg)
end
end
end
end
......@@ -2,36 +2,9 @@
module Gitlab
module Gpg
class Commit
include Gitlab::Utils::StrongMemoize
def initialize(commit)
@commit = commit
repo = commit.container.repository.raw_repository
@signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
lazy_signature
end
def signature_text
strong_memoize(:signature_text) do
@signature_data&.itself && @signature_data[0] # rubocop:disable Lint/SafeNavigationConsistency
end
end
def signed_text
strong_memoize(:signed_text) do
@signature_data&.itself && @signature_data[1] # rubocop:disable Lint/SafeNavigationConsistency
end
end
def has_signature?
!!(signature_text && signed_text)
end
class Commit < Gitlab::SignedCommit
def signature
return unless has_signature?
super
return @signature if @signature
......
# frozen_string_literal: true
module Gitlab
class SignedCommit
include Gitlab::Utils::StrongMemoize
def initialize(commit)
@commit = commit
if commit.project
repo = commit.project.repository.raw_repository
@signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
end
lazy_signature
end
def signature
return unless @commit.has_signature?
end
def signature_text
strong_memoize(:signature_text) do
@signature_data.itself ? @signature_data[0] : nil
end
end
def signed_text
strong_memoize(:signed_text) do
@signature_data.itself ? @signature_data[1] : nil
end
end
end
end
# frozen_string_literal: true
require 'openssl'
require 'digest'
module Gitlab
module X509
class Commit < Gitlab::SignedCommit
def signature
super
return @signature if @signature
cached_signature = lazy_signature&.itself
return @signature = cached_signature if cached_signature.present?
@signature = create_cached_signature!
end
def update_signature!(cached_signature)
cached_signature.update!(attributes)
@signature = cached_signature
end
private
def lazy_signature
BatchLoader.for(@commit.sha).batch do |shas, loader|
X509CommitSignature.by_commit_sha(shas).each do |signature|
loader.call(signature.commit_sha, signature)
end
end
end
def verified_signature
strong_memoize(:verified_signature) { verified_signature? }
end
def cert
strong_memoize(:cert) do
signer_certificate(p7) if valid_signature?
end
end
def cert_store
strong_memoize(:cert_store) do
store = OpenSSL::X509::Store.new
store.set_default_paths
# valid_signing_time? checks the time attributes already
# this flag is required, otherwise expired certificates would become
# unverified when notAfter within certificate attribute is reached
store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME
store
end
end
def p7
strong_memoize(:p7) do
pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----')
pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----')
OpenSSL::PKCS7.new(pkcs7_text)
rescue
nil
end
end
def valid_signing_time?
# rfc 5280 - 4.1.2.5 Validity
# check if signed_time is within the time range (notBefore/notAfter)
# non-rfc - git specific check: signed_time >= commit_time
p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) &&
p7.signers[0].signed_time >= @commit.created_at
end
def valid_signature?
p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY)
rescue
nil
end
def verified_signature?
# verify has multiple options but only a boolean return value
# so first verify without certificate chain
if valid_signature?
if valid_signing_time?
# verify with system certificate chain
p7.verify([], cert_store, signed_text)
else
false
end
else
nil
end
rescue
nil
end
def signer_certificate(p7)
p7.certificates.each do |cert|
next if cert.serial != p7.signers[0].serial
return cert
end
end
def certificate_crl
extension = get_certificate_extension('crlDistributionPoints')
extension.split('URI:').each do |item|
item.strip
if item.start_with?("http")
return item.strip
end
end
end
def get_certificate_extension(extension)
cert.extensions.each do |ext|
if ext.oid == extension
return ext.value
end
end
end
def issuer_subject_key_identifier
get_certificate_extension('authorityKeyIdentifier').gsub("keyid:", "").delete!("\n")
end
def certificate_subject_key_identifier
get_certificate_extension('subjectKeyIdentifier')
end
def certificate_issuer
cert.issuer.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_subject
cert.subject.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_email
get_certificate_extension('subjectAltName').split('email:')[1]
end
def issuer_attributes
return if verified_signature.nil?
{
subject_key_identifier: issuer_subject_key_identifier,
subject: certificate_issuer,
crl_url: certificate_crl
}
end
def certificate_attributes
return if verified_signature.nil?
issuer = X509Issuer.safe_create!(issuer_attributes)
{
subject_key_identifier: certificate_subject_key_identifier,
subject: certificate_subject,
email: certificate_email,
serial_number: cert.serial,
x509_issuer_id: issuer.id
}
end
def attributes
return if verified_signature.nil?
certificate = X509Certificate.safe_create!(certificate_attributes)
{
commit_sha: @commit.sha,
project: @commit.project,
x509_certificate_id: certificate.id,
verification_status: verification_status
}
end
def verification_status
if verified_signature && certificate_email == @commit.committer_email
:verified
else
:unverified
end
end
def create_cached_signature!
return if verified_signature.nil?
return X509CommitSignature.new(attributes) if Gitlab::Database.read_only?
X509CommitSignature.safe_create!(attributes)
end
end
end
end
......@@ -3194,6 +3194,12 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
msgid "Certificate Issuer"
msgstr ""
msgid "Certificate Subject"
msgstr ""
msgid "Change assignee"
msgstr ""
......@@ -11130,6 +11136,9 @@ msgstr ""
msgid "Learn more about the dependency list"
msgstr ""
msgid "Learn more about x509 signed commits"
msgstr ""
msgid "Learn more in the"
msgstr ""
......@@ -18173,6 +18182,9 @@ msgstr ""
msgid "Subgroups and projects"
msgstr ""
msgid "Subject Key Identifier:"
msgstr ""
msgid "Subkeys"
msgstr ""
......
......@@ -281,7 +281,7 @@ describe Projects::CompareController do
context 'when the user has access to the project' do
render_views
let(:signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'signature_commit') }
let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') }
before do
......
# frozen_string_literal: true
FactoryBot.define do
factory :x509_certificate do
subject_key_identifier { 'BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC' }
subject { 'CN=gitlab@example.org,OU=Example,O=World' }
email { 'gitlab@example.org' }
serial_number { 278969561018901340486471282831158785578 }
x509_issuer
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :x509_commit_signature do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
project
x509_certificate
verification_status { :verified }
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :x509_issuer do
subject_key_identifier { 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB' }
subject { 'CN=PKI,OU=Example,O=World' }
crl_url { 'http://example.com/pki.crl' }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::X509::Commit do
describe '#signature' do
let(:signature) { described_class.new(commit).signature }
let(:user1_certificate_attributes) do
{
subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier,
subject: X509Helpers::User1.certificate_subject,
email: X509Helpers::User1.certificate_email,
serial_number: X509Helpers::User1.certificate_serial
}
end
let(:user1_issuer_attributes) do
{
subject_key_identifier: X509Helpers::User1.issuer_subject_key_identifier,
subject: X509Helpers::User1.certificate_issuer,
crl_url: X509Helpers::User1.certificate_crl
}
end
shared_examples 'returns the cached signature on second call' do
it 'returns the cached signature on second call' do
x509_commit = described_class.new(commit)
expect(x509_commit).to receive(:create_cached_signature).and_call_original
signature
# consecutive call
expect(x509_commit).not_to receive(:create_cached_signature).and_call_original
signature
end
end
let!(:project) { create :project, :repository, path: X509Helpers::User1.path }
let!(:commit_sha) { X509Helpers::User1.commit }
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 'valid signature from known user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first }
let!(:user) { create(:user, email: X509Helpers::User1.emails.first) }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data
]
)
end
it 'returns an unverified signature' do
expect(signature).to have_attributes(
commit_sha: commit_sha,
project: project,
verification_status: 'unverified'
)
expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes)
expect(signature.persisted?).to be_truthy
end
end
context 'verified signature from known user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first }
let!(:user) { create(:user, email: X509Helpers::User1.emails.first) }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data
]
)
end
context 'with trusted certificate store' do
before do
store = OpenSSL::X509::Store.new
certificate = OpenSSL::X509::Certificate.new X509Helpers::User1.trust_cert
store.add_cert(certificate)
allow(OpenSSL::X509::Store).to receive(:new)
.and_return(
store
)
end
it 'returns a verified signature' do
expect(signature).to have_attributes(
commit_sha: commit_sha,
project: project,
verification_status: 'verified'
)
expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes)
expect(signature.persisted?).to be_truthy
end
end
context 'without trusted certificate within store' do
before do
store = OpenSSL::X509::Store.new
allow(OpenSSL::X509::Store).to receive(:new)
.and_return(
store
)
end
it 'returns an unverified signature' do
expect(signature).to have_attributes(
commit_sha: commit_sha,
project: project,
verification_status: 'unverified'
)
expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes)
expect(signature.persisted?).to be_truthy
end
end
end
context 'unverified signature from unknown user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data
]
)
end
it 'returns an unverified signature' do
expect(signature).to have_attributes(
commit_sha: commit_sha,
project: project,
verification_status: 'unverified'
)
expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes)
expect(signature.persisted?).to be_truthy
end
end
context 'invalid signature' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first }
let!(:user) { create(:user, email: X509Helpers::User1.emails.first) }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
# Corrupt the key
X509Helpers::User1.signed_commit_signature.tr('A', 'B'),
X509Helpers::User1.signed_commit_base_data
]
)
end
it 'returns nil' do
expect(described_class.new(commit).signature).to be_nil
end
end
context 'invalid commit message' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first }
let!(:user) { create(:user, email: X509Helpers::User1.emails.first) }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
X509Helpers::User1.signed_commit_signature,
# Corrupt the commit message
'x'
]
)
end
it 'returns nil' do
expect(described_class.new(commit).signature).to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb')
describe MigrateCreateCommitSignatureWorkerSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
include StubWorker
context 'when there are jobs in the queue' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
stub_worker(queue: 'create_commit_signature').perform_async('Something', [1])
stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('create_gpg_signature')).to eq 0
expect(sidekiq_queue_length('create_commit_signature')).to eq 2
end
end
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1])
described_class.new.down
expect(sidekiq_queue_length('create_gpg_signature')).to eq 1
expect(sidekiq_queue_length('create_commit_signature')).to eq 0
end
end
end
context 'when there are no jobs in the queues' do
it 'does not raise error when migrating up' do
expect { described_class.new.up }.not_to raise_error
end
it 'does not raise error when migrating down' do
expect { described_class.new.down }.not_to raise_error
end
end
end
......@@ -671,4 +671,25 @@ eos
expect(commit2.merge_requests).to contain_exactly(merge_request1)
end
end
describe 'signed commits' do
let(:gpg_signed_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
let(:unsigned_commit) { project.commit_by(oid: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') }
let!(:commit) { create(:commit, project: project) }
it 'returns signature_type properly' do
expect(gpg_signed_commit.signature_type).to eq(:PGP)
expect(x509_signed_commit.signature_type).to eq(:X509)
expect(unsigned_commit.signature_type).to eq(:NONE)
expect(commit.signature_type).to eq(:NONE)
end
it 'returns has_signature? properly' do
expect(gpg_signed_commit.has_signature?).to be_truthy
expect(x509_signed_commit.has_signature?).to be_truthy
expect(unsigned_commit.has_signature?).to be_falsey
expect(commit.has_signature?).to be_falsey
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe X509SerialNumberAttribute do
let(:model) { Class.new { include X509SerialNumberAttribute } }
before do
columns = [
double(:column, name: 'name', type: :text),
double(:column, name: 'serial_number', type: :binary)
]
allow(model).to receive(:columns).and_return(columns)
end
describe '#x509_serial_number_attribute' do
context 'when in non-production' do
before do
stub_rails_env('development')
end
context 'when the table exists' do
before do
allow(model).to receive(:table_exists?).and_return(true)
end
it 'defines a x509 serial number attribute for a binary column' do
expect(model).to receive(:attribute)
.with(:serial_number, an_instance_of(Gitlab::Database::X509SerialNumberAttribute))
model.x509_serial_number_attribute(:serial_number)
end
it 'raises ArgumentError when the column type is not :binary' do
expect { model.x509_serial_number_attribute(:name) }.to raise_error(ArgumentError)
end
end
context 'when the table does not exist' do
it 'allows the attribute to be added and issues a warning' do
allow(model).to receive(:table_exists?).and_return(false)
expect(model).not_to receive(:columns)
expect(model).to receive(:attribute)
expect(model).to receive(:warn)
model.x509_serial_number_attribute(:name)
end
end
context 'when the column does not exist' do
it 'allows the attribute to be added and issues a warning' do
allow(model).to receive(:table_exists?).and_return(true)
expect(model).to receive(:columns)
expect(model).to receive(:attribute)
expect(model).to receive(:warn)
model.x509_serial_number_attribute(:no_name)
end
end
context 'when other execeptions are raised' do
it 'logs and re-rasises the error' do
allow(model).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError.new('does not exist'))
expect(model).not_to receive(:columns)
expect(model).not_to receive(:attribute)
expect(Gitlab::AppLogger).to receive(:error)
expect { model.x509_serial_number_attribute(:name) }.to raise_error(ActiveRecord::NoDatabaseError)
end
end
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'defines a x509 serial number attribute' do
expect(model).not_to receive(:table_exists?)
expect(model).not_to receive(:columns)
expect(model).to receive(:attribute).with(:serial_number, an_instance_of(Gitlab::Database::X509SerialNumberAttribute))
model.x509_serial_number_attribute(:serial_number)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe X509Certificate do
describe 'validation' do
it { is_expected.to validate_presence_of(:subject_key_identifier) }
it { is_expected.to validate_presence_of(:subject) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_presence_of(:serial_number) }
it { is_expected.to validate_presence_of(:x509_issuer_id) }
end
describe 'associations' do
it { is_expected.to belong_to(:x509_issuer).required }
end
describe '.safe_create!' do
let(:subject_key_identifier) { 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD' }
let(:subject) { 'CN=gitlab@example.com,OU=Example,O=World' }
let(:email) { 'gitlab@example.com' }
let(:serial_number) { '123456789' }
let(:issuer) { create(:x509_issuer) }
let(:attributes) do
{
subject_key_identifier: subject_key_identifier,
subject: subject,
email: email,
serial_number: serial_number,
x509_issuer_id: issuer.id
}
end
it 'creates a new certificate if it was not found' do
expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
end
it 'assigns the correct attributes when creating' do
certificate = described_class.safe_create!(attributes)
expect(certificate.subject_key_identifier).to eq(subject_key_identifier)
expect(certificate.subject).to eq(subject)
expect(certificate.email).to eq(email)
end
end
describe 'validators' do
it 'accepts correct subject_key_identifier' do
subject_key_identifiers = [
'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB',
'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD'
]
subject_key_identifiers.each do |identifier|
expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_valid
end
end
it 'rejects invalid subject_key_identifier' do
subject_key_identifiers = [
'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB',
'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:GG',
'random string',
'12321342545356434523412341245452345623453542345234523453245'
]
subject_key_identifiers.each do |identifier|
expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_invalid
end
end
it 'accepts correct email address' do
emails = [
'smime@example.org',
'smime@example.com'
]
emails.each do |email|
expect(build(:x509_certificate, email: email)).to be_valid
end
end
it 'rejects invalid email' do
emails = [
'this is not an email',
'@example.org'
]
emails.each do |email|
expect(build(:x509_certificate, email: email)).to be_invalid
end
end
it 'accepts valid serial_number' do
expect(build(:x509_certificate, serial_number: 123412341234)).to be_valid
# rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum)
expect(build(:x509_certificate, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid
expect(build(:x509_certificate, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid
end
it 'rejects invalid serial_number' do
expect(build(:x509_certificate, serial_number: "sgsgfsdgdsfg")).to be_invalid
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe X509CommitSignature do
let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
let(:project) { create(:project, :public, :repository) }
let!(:commit) { create(:commit, project: project, sha: commit_sha) }
let(:x509_certificate) { create(:x509_certificate) }
let(:x509_signature) { create(:x509_commit_signature, commit_sha: commit_sha) }
it_behaves_like 'having unique enum values'
describe 'validation' do
it { is_expected.to validate_presence_of(:commit_sha) }
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:x509_certificate_id) }
end
describe 'associations' do
it { is_expected.to belong_to(:project).required }
it { is_expected.to belong_to(:x509_certificate).required }
end
describe '.safe_create!' do
let(:attributes) do
{
commit_sha: commit_sha,
project: project,
x509_certificate_id: x509_certificate.id,
verification_status: "verified"
}
end
it 'finds a signature by commit sha if it existed' do
x509_signature
expect(described_class.safe_create!(commit_sha: commit_sha)).to eq(x509_signature)
end
it 'creates a new signature if it was not found' do
expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
end
it 'assigns the correct attributes when creating' do
signature = described_class.safe_create!(attributes)
expect(signature.project).to eq(project)
expect(signature.commit_sha).to eq(commit_sha)
expect(signature.x509_certificate_id).to eq(x509_certificate.id)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe X509Issuer do
describe 'validation' do
it { is_expected.to validate_presence_of(:subject_key_identifier) }
it { is_expected.to validate_presence_of(:subject) }
it { is_expected.to validate_presence_of(:crl_url) }
end
describe '.safe_create!' do
let(:issuer_subject_key_identifier) { 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB' }
let(:issuer_subject) { 'CN=PKI,OU=Example,O=World' }
let(:issuer_crl_url) { 'http://example.com/pki.crl' }
let(:attributes) do
{
subject_key_identifier: issuer_subject_key_identifier,
subject: issuer_subject,
crl_url: issuer_crl_url
}
end
it 'creates a new issuer if it was not found' do
expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
end
it 'assigns the correct attributes when creating' do
issuer = described_class.safe_create!(attributes)
expect(issuer.subject_key_identifier).to eq(issuer_subject_key_identifier)
expect(issuer.subject).to eq(issuer_subject)
expect(issuer.crl_url).to eq(issuer_crl_url)
end
end
describe 'validators' do
it 'accepts correct subject_key_identifier' do
subject_key_identifiers = [
'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB',
'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD'
]
subject_key_identifiers.each do |identifier|
expect(build(:x509_issuer, subject_key_identifier: identifier)).to be_valid
end
end
it 'rejects invalid subject_key_identifier' do
subject_key_identifiers = [
'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB',
'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:GG',
'random string',
'12321342545356434523412341245452345623453542345234523453245'
]
subject_key_identifiers.each do |identifier|
expect(build(:x509_issuer, subject_key_identifier: identifier)).to be_invalid
end
end
it 'accepts valid crl_url' do
expect(build(:x509_issuer, crl_url: "https://pki.example.org")).to be_valid
end
it 'rejects invalid crl_url' do
expect(build(:x509_issuer, crl_url: "ht://pki.example.org")).to be_invalid
end
end
end
......@@ -214,23 +214,23 @@ describe Git::BranchHooksService do
end
end
describe 'GPG signatures' do
describe 'signatures' do
context 'when the commit has a signature' do
context 'when the signature is already cached' do
before do
create(:gpg_signature, commit_sha: commit.id)
end
it 'does not queue a CreateGpgSignatureWorker' do
expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
it 'does not queue a CreateCommitSignatureWorker' do
expect(CreateCommitSignatureWorker).not_to receive(:perform_async)
service.execute
end
end
context 'when the signature is not yet cached' do
it 'queues a CreateGpgSignatureWorker' do
expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id)
it 'queues a CreateCommitSignatureWorker' do
expect(CreateCommitSignatureWorker).to receive(:perform_async).with([commit.id], project.id)
service.execute
end
......@@ -240,7 +240,7 @@ describe Git::BranchHooksService do
.to receive(:shas_with_signatures)
.and_return([sample_commit.id, another_sample_commit.id])
expect(CreateGpgSignatureWorker)
expect(CreateCommitSignatureWorker)
.to receive(:perform_async)
.with([sample_commit.id, another_sample_commit.id], project.id)
......@@ -257,8 +257,8 @@ describe Git::BranchHooksService do
.and_return([])
end
it 'does not queue a CreateGpgSignatureWorker' do
expect(CreateGpgSignatureWorker)
it 'does not queue a CreateCommitSignatureWorker' do
expect(CreateCommitSignatureWorker)
.not_to receive(:perform_async)
.with(sample_commit.id, project.id)
......
# frozen_string_literal: true
module X509Helpers
module User1
extend self
def commit
'a4df3c87f040f5fa693d4d55a89b6af74e22cb56'
end
def path
'gitlab-test'
end
def trust_cert
<<~TRUSTCERTIFICATE
-----BEGIN CERTIFICATE-----
MIIGVTCCBD2gAwIBAgIEdikH4zANBgkqhkiG9w0BAQsFADCBmTELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoM
B1NpZW1lbnMxETAPBgNVBAUTCFpaWlpaWkExMR0wGwYDVQQLDBRTaWVtZW5zIFRy
dXN0IENlbnRlcjEiMCAGA1UEAwwZU2llbWVucyBSb290IENBIFYzLjAgMjAxNjAe
Fw0xNjA2MDYxMzMwNDhaFw0yODA2MDYxMzMwNDhaMIGZMQswCQYDVQQGEwJERTEP
MA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2ll
bWVuczERMA8GA1UEBRMIWlpaWlpaQTExHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3Qg
Q2VudGVyMSIwIAYDVQQDDBlTaWVtZW5zIFJvb3QgQ0EgVjMuMCAyMDE2MIICIjAN
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp2k2PcfRBu1yeXUxG3UoEDDTFtgF
zGVNIq4j4g6niE7hxZzoferzgC6bK3y+lOQFfNkctFzjq6N+JvH535KnN4vXvNoO
/Rvrn38XtUC8ms2/1MlzvFDMh0Rt1HzemJYsSUXPvj5EMjGVzeQu1/GZhN6XlRrc
SgMSeuwAGN4IX/0QIyxaArxlDZks6zSOA+s9t2PBp6vPZcqA9y4RZLc33nQmdwZg
onEYK55xS1QFY2/zuZGQtB73e69IsrAxP+ZzrivlpbgKkEb1kt0qd7rLkp/HnM9J
IDFc6uo8dAUCA/oR40Yfe2+8hyKoTrFbTvxC2SqxoBolAemZ2rnckuQ1RInbCQNp
pBJJr/Hg78yvIp65gP6mZsyhL6ZLLXjL+ICIUTU86OedkJ7j9o4vdrwBn8AugENy
8jAMu06k9CFbe7QoEynlRvm5VoYMSBsMqn7lAmuBcuMHdEdXu/qN/ULRLGkx1QRc
gqf7+QszYla8QEaTtxQKWfdAU0Fyg0ROagrBtFjuDjsMeLK6LM17K3FFM3pghISj
o4A8+y2fSbKKnMvU1z3Zey6vnGSwZKOxMJy5/aWuERbegQ07iH0jaA7S/gKZhOKO
uDHD9qOBYfKou6wC+xdWyPGFPOq8BQRkWrSEeQW9FxhyYhhcCdcRh+hpZ4eHgRLM
KkiFrljndwyB4eUCAwEAAaOBojCBnzAfBgNVHSMEGDAWgBRwbaBQ7KnQLGedGRX+
/QRzNcPi1DAPBgNVHRMBAf8EBTADAQH/MDwGA1UdIAQ1MDMwMQYEVR0gADApMCcG
CCsGAQUFBwIBFhtodHRwOi8vd3d3LnNpZW1lbnMuY29tL3BraS8wDgYDVR0PAQH/
BAQDAgEGMB0GA1UdDgQWBBRwbaBQ7KnQLGedGRX+/QRzNcPi1DANBgkqhkiG9w0B
AQsFAAOCAgEAHAxI694Yl16uKvWUdGDoglYLXmTxkVHOSci3TxzdEsAJ6WEf7kbj
6zSQxGcAOz7nvto80rOZzlCluoO5K5fD7a4nEKl+tuBPrgtcEE8nkspPJF6DwjHQ
Lmh219YxktZ1D7egLaRCGvxbPjkb3Wuh4vLqzZHr8twcauMxMyqRTN5F2+F43MY0
AeBIb9QIMYsxxLBxsSeg4aajGwhdj5FmDFUFbGlyIjd0FfnXxvMuRtWpUWOu4Tya
kA0AX/q6uM/L9SFIwmzTO7+2AHW/m/HrCmWb6R4VYWAgppp+jhUViW5l1uLB3i4m
5IaJHZilU/DwQ5FnkuP2xqLvZ7AF3uXBlldOAbE1327uGIhYgp40Oi7PIHH+vgwg
JOXQJ3SMwEzYmxCNsyLKAJb2Gs1IpwEpz7lpitl7i/DeUlPZSAo+1SLzc7P35muX
ukCeh1vR7LJdCeYQpDpKeUYjKaNXr2/rZlMFmOGXLBKQvTNcI2I5WTIbVQ1sxhWN
0FS+INH6jUypiwh0WH2R1Bo0HY3Lq4zJJ3Ct/12ocQ78+JfENXI8glOs3H07jyng
afEj0ba23cn4HnV8s4T0jt8KZYlNkSNlSJ5kgTaZjmdLbTbt24OO4f3WNRrINwKC
VzrN1ydSBGHNOsb/muR5axK/dHN2TEycRJPO6kSaVclLhMTxEmhRBUE=
-----END CERTIFICATE-----
TRUSTCERTIFICATE
end
def signed_commit_signature
<<~SIGNATURE
-----BEGIN SIGNED MESSAGE-----
MIISUgYJKoZIhvcNAQcCoIISQzCCEj8CAQExDTALBglghkgBZQMEAgEwCwYJKoZI
hvcNAQcBoIIP3TCCB2kwggVRoAMCAQICBGvn1/4wDQYJKoZIhvcNAQELBQAwgZ8x
CzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVu
MRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBMjEdMBsGA1UECwwU
U2llbWVucyBUcnVzdCBDZW50ZXIxKDAmBgNVBAMMH1NpZW1lbnMgSXNzdWluZyBD
QSBFRSBBdXRoIDIwMTYwHhcNMTcwMjAzMDY1MzUyWhcNMjAwMjAzMDY1MzUyWjBb
MREwDwYDVQQFEwhaMDAwTldESDEOMAwGA1UEKgwFUm9nZXIxDjAMBgNVBAQMBU1l
aWVyMRAwDgYDVQQKDAdTaWVtZW5zMRQwEgYDVQQDDAtNZWllciBSb2dlcjCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIpqbpRAtn+vetgVb+APuoVOytZx
TmfWovp22nsmJQwE8ZgrJihRjIez0wjD3cvSREvWUXsvbiyxrSHmmwycRCV9YGi1
Y9vaYRKOrWhT64Xv6wq6oq8VoA5J3z6V5P6Tkj7g9Q3OskRuSbhFQY89VUdsea+N
mcv/XrwtQR0SekfSZw9k0LhbauE69SWRV26O03raengjecbbkS+GTlP30/CqPzzQ
4Ac2TmmVF7RlkGRB05mJqHS+nDK7Lmcr7jD0e92YW+v8Lft4Qu3MpFTYVa7zk712
5xWAgedyOaJb6TpJEz8KRX8v3i0PilQnuKAqZFkLjNcydOox0AtYRW1P2iMCAwEA
AaOCAu4wggLqMB0GA1UdDgQWBBTsALUoAlzTpaGrwqE0gYSqv5vP+DBDBgNVHREE
PDA6oCMGCisGAQQBgjcUAgOgFQwTci5tZWllckBzaWVtZW5zLmNvbYETci5tZWll
ckBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMCB4AwKQYDVR0lBCIwIAYIKwYBBQUH
AwIGCCsGAQUFBwMEBgorBgEEAYI3FAICMIHKBgNVHR8EgcIwgb8wgbyggbmggbaG
Jmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTIuY3JshkFsZGFwOi8v
Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTIsTD1QS0k/Y2VydGlmaWNhdGVSZXZv
Y2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEyLG89
VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8
MDoGDSsGAQQBoWkHAgIDAQEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVt
ZW5zLmNvbS9wa2kvMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUvb0qQyI9SEpX
fpgxF6lwne6fqJkwggEEBggrBgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0
cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBMi5jcnQwQQYIKwYBBQUHMAKG
NWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMixMPVBLST9jQUNlcnRp
ZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049Wlpa
WlpaQTIsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRo
dHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
BQADggIBAFY2sbX8DKjKlp0OdH+7Ak21ZdRr6p6JIXzQShWpuFr3wYTpM47+WYVe
arBekf8eS08feM+TWw6FHt/VNMpn5fLr20jHn7h+j3ClerAxQbx8J6BxhwJ/4DMy
0cCdbe/fpfJyD/8TGdjnxwAgoq9iPuy1ueVnevygnLcuq1+se6EWJm9v1zrwB0LH
rE4/NaSCi06+KGg0D9yiigma9yErRZCiaFvqYXUEl7iGpu2OM9o38gZfGzkKaPtQ
e9BzRs6ndmvNpQQGLXvOlHn6DIsOuBHJp66A+wumRO2AC8rs1rc4NAIjCFRrz8k1
kzb+ibFiTklWG69+At5/nb06BO/0ER4U18sSpmvOsFKNKPXzLkAn8O8ZzB+8afxy
egiIJFxYaqoJcQq3CCv8Xp7tp6I+ojr1ui0jK0yqJq6QfgS8FCXIJ+EErNYuoerx
ba6amD83e524sdMhCfD5dw6IeEY7LUl465ifUm+v5W3jStfa+0cQXnLZNGsC85nP
Lw5cXVIE3LfoSO3kWH45MfcX32fuqmyP2N3k+/+IOfUpSdT1iR1pEu0g/mow7lGj
CZngjmMpoto/Qi3l/n1KPWfmB09FZlUhHcGsHbK8+mrkqpv6HW3tKDSorah98aLM
Wvu1IXTrU9fOyBqt92i0e5buH+/9NHia0i6k79kwQy5wu6Q21GgUMIIIbDCCBlSg
AwIBAgIEL4jNizANBgkqhkiG9w0BAQsFADCBmTELMAkGA1UEBhMCREUxDzANBgNV
BAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1lbnMx
ETAPBgNVBAUTCFpaWlpaWkExMR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRl
cjEiMCAGA1UEAwwZU2llbWVucyBSb290IENBIFYzLjAgMjAxNjAeFw0xNjA3MjAx
MzA5MDhaFw0yMjA3MjAxMzA5MDhaMIGfMQswCQYDVQQGEwJERTEPMA0GA1UECAwG
QmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2llbWVuczERMA8G
A1UEBRMIWlpaWlpaQTIxHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3QgQ2VudGVyMSgw
JgYDVQQDDB9TaWVtZW5zIElzc3VpbmcgQ0EgRUUgQXV0aCAyMDE2MIICIjANBgkq
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy1aUq88DjZYPge0vZnAr3KJHmMi0o5mp
hy54Xr592Vtf8u/B3TCyD+iGCYANPYUq4sG18qXcVxGadz7zeEm6RI7jKKl3URAv
zFGiYForZE0JKxwo956T/diLLpH1vHEQDbp8AjNK7aGoltZnm/Jn6IVQy9iBY0SE
lRIBhUlppS4/J2PHtKEvQVYJfkAwTtHuGpvPaesoJ8bHA0KhEZ4+/kIYQebaNDf0
ltTmXd4Z8zeUhE25d9MzoFnQUg+F01ewMfc0OsEFheKWP6dmo0MSLWARXxjI3K2R
THtJU5hxjb/+SA2wlfpqwNIAkTECDBfqYxHReAT8PeezvzEkNZ9RrXl9qj0Cm2iZ
AjY1SL+asuxrGvFwEW/ZKJ2ARY/ot1cHh/I79srzh/jFieShVHbT6s6fyKXmkUjB
OEnybUKUqcvNuOXnwEiJ/9jKT5UVBWTDxbEQucAarVNFBEf557o9ievbT+VAZKZ8
F4tJge6jl2y19eppflresr7Xui9wekK2LYcLOF3X/MOCFq/9VyQDyE7X9KNGtEx7
4V6J2QpbbRJryvavh3b0eQEtqDc65eiEaP8awqOErN8EEYh7Gdx4Um3QFcm1TBhk
ZTdQdLlWv4LvIBnXiBEWRczQYEIm5wv5ZkyPwdL39Xwc72esPPBu8FtQFVcQlRdG
I2t5Ywefq48CAwEAAaOCArIwggKuMIIBBQYIKwYBBQUHAQEEgfgwgfUwQQYIKwYB
BQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMSxMPVBLST9j
QUNlcnRpZmljYXRlMDIGCCsGAQUFBzAChiZodHRwOi8vYWguc2llbWVucy5jb20v
cGtpP1paWlpaWkExLmNydDBKBggrBgEFBQcwAoY+bGRhcDovL2FsLnNpZW1lbnMu
Y29tL3VpZD1aWlpaWlpBMSxvPVRydXN0Y2VudGVyP2NBQ2VydGlmaWNhdGUwMAYI
KwYBBQUHMAGGJGh0dHA6Ly9vY3NwLnBraS1zZXJ2aWNlcy5zaWVtZW5zLmNvbTAf
BgNVHSMEGDAWgBRwbaBQ7KnQLGedGRX+/QRzNcPi1DASBgNVHRMBAf8ECDAGAQH/
AgEAMEAGA1UdIAQ5MDcwNQYIKwYBBAGhaQcwKTAnBggrBgEFBQcCARYbaHR0cDov
L3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHHBgNVHR8Egb8wgbwwgbmggbaggbOGP2xk
YXA6Ly9jbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMSxMPVBLST9hdXRob3JpdHlS
ZXZvY2F0aW9uTGlzdIYmaHR0cDovL2NoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpB
MS5jcmyGSGxkYXA6Ly9jbC5zaWVtZW5zLmNvbS91aWQ9WlpaWlpaQTEsbz1UcnVz
dGNlbnRlcj9hdXRob3JpdHlSZXZvY2F0aW9uTGlzdDAzBgNVHSUELDAqBggrBgEF
BQcDAgYIKwYBBQUHAwQGCisGAQQBgjcUAgIGCCsGAQUFBwMJMA4GA1UdDwEB/wQE
AwIBBjAdBgNVHQ4EFgQUvb0qQyI9SEpXfpgxF6lwne6fqJkwDQYJKoZIhvcNAQEL
BQADggIBAEQB0qDUmU8rX9KVJA/0zxJUmIeE9zeldih8TKrf4UNzS1+2Cqn4agO7
MxRG1d52/pL4uKenffwwYy2dP912PwLjCDOL7jvojjQKx/qpVUXF7XWsg8hAQec3
7Ras/jGPcPQ3OehbkcKcmXI4MrF0Haqo3q1n29gjlJ0fGn2fF1/CBnybPuODAjWG
o9mZodXfz0woGSxkftC6nTmAV2GCvIU+j5hNKpzEzo8c1KwLVeXtB4PAqioRW1BX
Ngjc7HQbvX/C39RnpOM3RdITw2KKXFxeKBMXdiDuFz/2CzO8HxKH9EVWEcSRbTnd
E5iEB4CZzcvfzl9X5AwrKkiIziOiEoiv21ooWeFWfR9V2dgYIE7G1TFwsQ4p0/w5
xBHSzqP8TCJp1MQTw42/t8uUXoFEGqk5FKQWoIaFf7N//FLAn8r+7vxNhF5s+tMl
VsdKnXn3q8THB3JSnbb/AWGL9rjPK3vh2d3c0I5cWuKXexPLp74ynl2XUbiOXKE7
XPUZ9qgK0G9JrrFMm4x1aID9Y9jqYeEz6krYjdFHo5BOVGso6SqWVJE48TxJ5KVv
FUb4OxhOAw118Tco0XA7H1G3c2/AKJvIku3cRuj8eLe/cpKqUqQl8uikIZs7POaO
+9eJsOnNPmUiwumJgwAo3Ka4ALteKZLbGmKvuo/2ueKCQ29F5rnOMYICOzCCAjcC
AQEwgagwgZ8xCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcM
CE11ZW5jaGVuMRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBMjEd
MBsGA1UECwwUU2llbWVucyBUcnVzdCBDZW50ZXIxKDAmBgNVBAMMH1NpZW1lbnMg
SXNzdWluZyBDQSBFRSBBdXRoIDIwMTYCBGvn1/4wCwYJYIZIAWUDBAIBoGkwHAYJ
KoZIhvcNAQkFMQ8XDTE5MDYyMDEwNDIwNlowLwYJKoZIhvcNAQkEMSIEIHPHp00z
IZ93dAl/uwOnixzuAtf1fUTyxFFaq/5yzc+0MBgGCSqGSIb3DQEJAzELBgkqhkiG
9w0BBwEwCwYJKoZIhvcNAQEBBIIBAD8Or5F/A/vpeNPv1YOrGzTrMU5pbn6o8t2+
Hqn+hAdjbD26HqjYQN/nyXNBpgXiV4P5vEVNVpmViAAXGsWKM3BJx7GdH/uUwDnj
upvoViXYtzQ92UC2Xzqo7uOg2ryMbDIFNfLosvy4a7NfDLYoMsVYrgOKpDrfOLsS
1VNUjlyftm7vKigkJLrPIEmXrZSVEqsdKvFhcSxS55lm0lVd/fTCAi7TXR2FZWbc
TrsTrZx2YdIJDwN04szzBjnQ7yJ4jBLYz1GMBe22xDD10UA4XdBYK07rkcabrv/t
kUMI7uN/KeiKPeSvWCn3AUqH6TIFa9WU+tI4U2A2BsUMn6Bq9TY=
-----END SIGNED MESSAGE-----
SIGNATURE
end
def signed_commit_base_data
<<~SIGNEDDATA
tree 84c167013d2ee86e8a88ac6011df0b178d261a23
parent e63f41fe459e62e1228fcef60d7189127aeba95a
author Roger Meier <r.meier@siemens.com> 1561027326 +0200
committer Roger Meier <r.meier@siemens.com> 1561027326 +0200
feat: add a smime signed commit
SIGNEDDATA
end
def certificate_crl
'http://ch.siemens.com/pki?ZZZZZZA2.crl'
end
def certificate_serial
1810356222
end
def certificate_subject_key_identifier
'EC:00:B5:28:02:5C:D3:A5:A1:AB:C2:A1:34:81:84:AA:BF:9B:CF:F8'
end
def issuer_subject_key_identifier
'BD:BD:2A:43:22:3D:48:4A:57:7E:98:31:17:A9:70:9D:EE:9F:A8:99'
end
def certificate_email
'r.meier@siemens.com'
end
def certificate_issuer
'CN=Siemens Issuing CA EE Auth 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA2,O=Siemens,L=Muenchen,ST=Bayern,C=DE'
end
def certificate_subject
'CN=Meier Roger,O=Siemens,SN=Meier,GN=Roger,serialNumber=Z000NWDH'
end
def names
['Roger Meier']
end
def emails
['r.meier@siemens.com']
end
end
end
......@@ -2,13 +2,14 @@
require 'spec_helper'
describe CreateGpgSignatureWorker do
describe CreateCommitSignatureWorker do
let(:project) { create(:project, :repository) }
let(:commits) { project.repository.commits('HEAD', limit: 3).commits }
let(:commit_shas) { commits.map(&:id) }
let(:gpg_commit) { instance_double(Gitlab::Gpg::Commit) }
let(:x509_commit) { instance_double(Gitlab::X509::Commit) }
context 'when GpgKey is found' do
context 'when a signature is found' do
before do
allow(Project).to receive(:find_by).with(id: project.id).and_return(project)
allow(project).to receive(:commits_by).with(oids: commit_shas).and_return(commits)
......@@ -18,6 +19,7 @@ describe CreateGpgSignatureWorker do
it 'calls Gitlab::Gpg::Commit#signature' do
commits.each do |commit|
allow(commit).to receive(:signature_type).and_return(:PGP)
expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit).once
end
......@@ -31,13 +33,46 @@ describe CreateGpgSignatureWorker do
allow(Gitlab::Gpg::Commit).to receive(:new).and_return(gpg_commit)
allow(Gitlab::Gpg::Commit).to receive(:new).with(commits.first).and_raise(StandardError)
allow(commits[1]).to receive(:signature_type).and_return(:PGP)
allow(commits[2]).to receive(:signature_type).and_return(:PGP)
expect(gpg_commit).to receive(:signature).twice
subject
end
it 'calls Gitlab::X509::Commit#signature' do
commits.each do |commit|
allow(commit).to receive(:signature_type).and_return(:X509)
expect(Gitlab::X509::Commit).to receive(:new).with(commit).and_return(x509_commit).once
end
expect(x509_commit).to receive(:signature).exactly(commits.size).times
subject
end
it 'can recover from exception and continue the X509 signature process' do
allow(x509_commit).to receive(:signature)
allow(Gitlab::X509::Commit).to receive(:new).and_return(x509_commit)
allow(Gitlab::X509::Commit).to receive(:new).with(commits.first).and_raise(StandardError)
allow(commits[1]).to receive(:signature_type).and_return(:X509)
allow(commits[2]).to receive(:signature_type).and_return(:X509)
expect(x509_commit).to receive(:signature).twice
subject
end
end
context 'handles when a string is passed in for the commit SHA' do
before do
allow(Project).to receive(:find_by).with(id: project.id).and_return(project)
allow(project).to receive(:commits_by).with(oids: Array(commit_shas.first)).and_return(commits)
allow(commits.first).to receive(:signature_type).and_return(:PGP)
end
it 'creates a signature once' do
allow(Gitlab::Gpg::Commit).to receive(:new).with(commits.first).and_return(gpg_commit)
......@@ -67,5 +102,11 @@ describe CreateGpgSignatureWorker do
described_class.new.perform(commit_shas, nonexisting_project_id)
end
it 'does not call Gitlab::X509::Commit#signature' do
expect_any_instance_of(Gitlab::X509::Commit).not_to receive(:signature)
described_class.new.perform(commit_shas, nonexisting_project_id)
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