summaryrefslogtreecommitdiff
path: root/lib/gitlab/gpg/commit.rb
blob: 1d317c389d2febef8edef9e160fbe2e57c8ee154 (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
130
131
132
133
134
135
136
137
138
139
140
141
# frozen_string_literal: true

module Gitlab
  module Gpg
    class Commit
      include Gitlab::Utils::StrongMemoize

      def initialize(commit)
        @commit = commit

        repo = commit.project.repository.raw_repository
        @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
      end

      def signature_text
        strong_memoize(:signature_text) do
          @signature_data&.itself && @signature_data[0]
        end
      end

      def signed_text
        strong_memoize(:signed_text) do
          @signature_data&.itself && @signature_data[1]
        end
      end

      def has_signature?
        !!(signature_text && signed_text)
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def signature
        return unless has_signature?

        return @signature if @signature

        cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
        return @signature = cached_signature if cached_signature.present?

        @signature = create_cached_signature!
      end
      # rubocop: enable CodeReuse/ActiveRecord

      def update_signature!(cached_signature)
        using_keychain do |gpg_key|
          cached_signature.update!(attributes(gpg_key))
          @signature = cached_signature
        end
      end

      private

      def using_keychain
        Gitlab::Gpg.using_tmp_keychain do
          # first we need to get the fingerprint from the signature to query the gpg
          # key belonging to the fingerprint.
          # This way we can add the key to the temporary keychain and extract
          # the proper signature.
          # NOTE: the invoked method is #fingerprint but versions of GnuPG
          # prior to 2.2.13 return 16 characters (the format used by keyid)
          # instead of 40.
          fingerprint = verified_signature&.fingerprint

          break unless fingerprint

          gpg_key = find_gpg_key(fingerprint)

          if gpg_key
            Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
            clear_memoization(:verified_signature)
          end

          yield gpg_key
        end
      end

      def verified_signature
        strong_memoize(:verified_signature) { gpgme_signature }
      end

      def gpgme_signature
        GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
          # Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-ce/issues/54932
          break verified_signature
        end
      rescue GPGME::Error
        nil
      end

      def create_cached_signature!
        using_keychain do |gpg_key|
          attributes = attributes(gpg_key)
          break GpgSignature.new(attributes) if Gitlab::Database.read_only?

          GpgSignature.safe_create!(attributes)
        end
      end

      def attributes(gpg_key)
        user_infos = user_infos(gpg_key)
        verification_status = verification_status(gpg_key)

        {
          commit_sha: @commit.sha,
          project: @commit.project,
          gpg_key: gpg_key,
          gpg_key_primary_keyid: gpg_key&.keyid || verified_signature&.fingerprint,
          gpg_key_user_name: user_infos[:name],
          gpg_key_user_email: user_infos[:email],
          verification_status: verification_status
        }
      end

      def verification_status(gpg_key)
        return :unknown_key unless gpg_key
        return :unverified_key unless gpg_key.verified?
        return :unverified unless verified_signature&.valid?

        if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
          :verified
        elsif gpg_key.user.all_emails.include?(@commit.committer_email)
          :same_user_different_email
        else
          :other_user
        end
      end

      def user_infos(gpg_key)
        gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
      end

      def find_gpg_key(fingerprint)
        if fingerprint.length > 16
          GpgKey.find_by_fingerprint(fingerprint) || GpgKeySubkey.find_by_fingerprint(fingerprint)
        else
          GpgKey.find_by_primary_keyid(fingerprint) || GpgKeySubkey.find_by_keyid(fingerprint)
        end
      end
    end
  end
end