summaryrefslogtreecommitdiff
path: root/app/models/gpg_key.rb
blob: 116beac5c2a7363373575ed1159f97bf374b00bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# frozen_string_literal: true

class GpgKey < ApplicationRecord
  KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
  KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze

  include ShaAttribute

  sha_attribute :primary_keyid
  sha_attribute :fingerprint

  belongs_to :user
  has_many :gpg_signatures
  has_many :subkeys, class_name: 'GpgKeySubkey'

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

  validates :user, presence: true

  validates :key,
    presence: true,
    uniqueness: true,
    format: {
      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}'"
    }

  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) }

  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) }

  before_validation :extract_fingerprint, :extract_primary_keyid
  after_commit :update_invalid_gpg_signatures, on: :create
  after_create :generate_subkeys

  def primary_keyid
    super&.upcase
  end
  alias_method :keyid, :primary_keyid

  def fingerprint
    super&.upcase
  end

  def key=(value)
    super(value&.strip)
  end

  def keyids
    [keyid].concat(subkeys.map(&:keyid))
  end

  def user_infos
    @user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
  end

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

  def emails_with_verified_status
    user_infos.map do |user_info|
      [
        user_info[:email],
        user.verified_email?(user_info[:email])
      ]
    end.to_h
  end

  def verified?
    emails_with_verified_status.values.any?
  end

  def verified_and_belongs_to_email?(email)
    emails_with_verified_status.fetch(email.downcase, false)
  end

  def update_invalid_gpg_signatures
    InvalidGpgSignatureUpdateWorker.perform_async(self.id)
  end

  def revoke
    GpgSignature
      .with_key_and_subkeys(self)
      .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
      .update_all(
        gpg_key_id: nil,
        gpg_key_subkey_id: nil,
        verification_status: GpgSignature.verification_statuses[:unknown_key],
        updated_at: Time.zone.now
      )

    destroy
  end

  private

  def extract_fingerprint
    # we can assume that the result only contains one item as the validation
    # only allows one key
    self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
  end

  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

  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