gpg_key.rb 3.2 KB
Newer Older
1 2
# frozen_string_literal: true

Alexis Reigel's avatar
Alexis Reigel committed
3 4
class GpgKey < ActiveRecord::Base
  KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
5
  KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze
Alexis Reigel's avatar
Alexis Reigel committed
6

7 8 9 10 11
  include ShaAttribute

  sha_attribute :primary_keyid
  sha_attribute :fingerprint

Alexis Reigel's avatar
Alexis Reigel committed
12
  belongs_to :user
13
  has_many :gpg_signatures
14
  has_many :subkeys, class_name: 'GpgKeySubkey'
Alexis Reigel's avatar
Alexis Reigel committed
15

16 17
  scope :with_subkeys, -> { includes(:subkeys) }

18 19
  validates :user, presence: true

Alexis Reigel's avatar
Alexis Reigel committed
20 21 22 23
  validates :key,
    presence: true,
    uniqueness: true,
    format: {
24 25
      with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX})(?!#{KEY_SUFFIX}).)+#{KEY_SUFFIX}\Z/m,
      message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}' and ends with '#{KEY_SUFFIX}'"
26
    }
Alexis Reigel's avatar
Alexis Reigel committed
27

28 29 30 31 32 33 34
  validates :fingerprint,
    presence: true,
    uniqueness: true,
    # only validate when the `key` is valid, as we don't want the user to show
    # the error about the fingerprint
    unless: -> { errors.has_key?(:key) }

35 36 37 38 39 40 41
  validates :primary_keyid,
    presence: true,
    uniqueness: true,
    # only validate when the `key` is valid, as we don't want the user to show
    # the error about the fingerprint
    unless: -> { errors.has_key?(:key) }

42
  before_validation :extract_fingerprint, :extract_primary_keyid
43
  after_commit :update_invalid_gpg_signatures, on: :create
44
  after_create :generate_subkeys
Alexis Reigel's avatar
Alexis Reigel committed
45

46 47 48
  def primary_keyid
    super&.upcase
  end
49
  alias_method :keyid, :primary_keyid
50 51 52 53 54

  def fingerprint
    super&.upcase
  end

Alexis Reigel's avatar
Alexis Reigel committed
55
  def key=(value)
Alexis Reigel's avatar
Alexis Reigel committed
56
    super(value&.strip)
Alexis Reigel's avatar
Alexis Reigel committed
57 58
  end

59 60 61 62
  def keyids
    [keyid].concat(subkeys.map(&:keyid))
  end

63 64 65 66 67 68
  def user_infos
    @user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
  end

  def verified_user_infos
    user_infos.select do |user_info|
69
      user.verified_email?(user_info[:email])
70
    end
71 72
  end

73
  def emails_with_verified_status
74
    user_infos.map do |user_info|
Alexis Reigel's avatar
Alexis Reigel committed
75
      [
76
        user_info[:email],
77
        user.verified_email?(user_info[:email])
Alexis Reigel's avatar
Alexis Reigel committed
78
      ]
79
    end.to_h
Alexis Reigel's avatar
Alexis Reigel committed
80 81
  end

82
  def verified?
83
    emails_with_verified_status.values.any?
84 85
  end

86
  def verified_and_belongs_to_email?(email)
87
    emails_with_verified_status.fetch(email.downcase, false)
88 89
  end

90
  def update_invalid_gpg_signatures
91
    InvalidGpgSignatureUpdateWorker.perform_async(self.id)
92 93
  end

94
  def revoke
95
    GpgSignature
96
      .with_key_and_subkeys(self)
97 98 99
      .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
      .update_all(
        gpg_key_id: nil,
100
        gpg_key_subkey_id: nil,
101 102 103
        verification_status: GpgSignature.verification_statuses[:unknown_key],
        updated_at: Time.zone.now
      )
104 105 106 107

    destroy
  end

Alexis Reigel's avatar
Alexis Reigel committed
108 109 110 111 112
  private

  def extract_fingerprint
    # we can assume that the result only contains one item as the validation
    # only allows one key
113
    self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
Alexis Reigel's avatar
Alexis Reigel committed
114
  end
115

116 117 118 119 120
  def extract_primary_keyid
    # we can assume that the result only contains one item as the validation
    # only allows one key
    self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
  end
121 122 123 124

  def generate_subkeys
    gpg_subkeys = Gitlab::Gpg.subkeys_from_key(key)

Rubén Dávila's avatar
Rubén Dávila committed
125
    gpg_subkeys[primary_keyid]&.each do |subkey_data|
126
      subkeys.create!(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
127 128
    end
  end
Alexis Reigel's avatar
Alexis Reigel committed
129
end