summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/ssh/signature_spec.rb
blob: ee9b38cae7d7142abc4f43f772867ff05d09c766 (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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Ssh::Signature, feature_category: :source_code_management do
  # ssh-keygen -t ed25519
  let_it_be(:committer_email) { 'ssh-commit-test@example.com' }
  let_it_be(:public_key_text) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHZ8NHEnCIpC4mnot+BRxv6L+fq+TnN1CgsRrHWLmfwb' }
  let_it_be_with_reload(:user) { create(:user, email: committer_email) }
  let_it_be_with_reload(:key) { create(:key, usage_type: :signing, key: public_key_text, user: user) }

  let(:signed_text) { 'This message was signed by an ssh key' }

  let(:signature_text) do
    # ssh-keygen -Y sign -n git -f id_test message.txt
    <<~SIG
      -----BEGIN SSH SIGNATURE-----
      U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
      5Oc3UKCxGsdYuZ/BsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
      AAAAQDWOEauf0jXyA9caa5bOgK5QZD6c69pm+EbG3GMw5QBL3N/Gt+r413McCSJFohWWBk
      Lxemg8NzZ0nB7lTFbaxQc=
      -----END SSH SIGNATURE-----
    SIG
  end

  subject(:signature) do
    described_class.new(
      signature_text,
      signed_text,
      committer_email
    )
  end

  shared_examples 'verified signature' do
    it 'reports verified status' do
      expect(signature.verification_status).to eq(:verified)
    end
  end

  shared_examples 'unverified signature' do
    it 'reports unverified status' do
      expect(signature.verification_status).to eq(:unverified)
    end
  end

  describe 'signature verification' do
    context 'when signature is valid and user email is verified' do
      it_behaves_like 'verified signature'
    end

    context 'when using an RSA key' do
      let(:public_key_text) do
        <<~KEY.delete("\n")
          ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDkq6ko8LMxf2NwyJKh+77KSDc7/ynPgUJD
          IopkhqftuHFYe2Y+V3MBJnpzfSRwR2xGfXQUUzLU9AGyfZIO/ZLK2yvfhlO3k//5PbAaZb3y
          urlnF9T1d2nhtfi8wuzsEn7Boh6qdoWPFIsloAL/X0PXH1HWKmzyNer92HKGrnWFfaaEMo0n
          T3ureAhRG4IONyUcOK+DyoH+YbxXSlHnLO2oHHlWaP9RrJCHbfAQbfDhaZCI0cNkXXOwUwA4
          yWGzDibfXZTvaYxpjbz1xoHmCAq8IrobCgkQaEg3PH3vPGnbP0TpViXjMnZyBZyT7tg9WHBV
          kAsl0CizyUgZHPAPYuqKy5JNlnjVjeqYeIgdN4Tj7hpJ1n0hVpRk4zQNYRmAAj3GNqgPAsd0
          3i4rW8cqmhO0fmhP5DgQ7Mt5S9AgcTcCr6niPacK34XrwKiRjxXmCLjr36q8wuRU3QdMt+MK
          Zxk/qJdAUIltz+nuGiwct0w+sWefYzmiRXu6hljBBrRAvnU=
        KEY
      end

      let(:signature_text) do
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAOSrqSjwszF/Y3DIkqH7vs
          pINzv/Kc+BQkMiimSGp+24cVh7Zj5XcwEmenN9JHBHbEZ9dBRTMtT0AbJ9kg79ksrbK9+G
          U7eT//k9sBplvfK6uWcX1PV3aeG1+LzC7OwSfsGiHqp2hY8UiyWgAv9fQ9cfUdYqbPI16v
          3YcoaudYV9poQyjSdPe6t4CFEbgg43JRw4r4PKgf5hvFdKUecs7agceVZo/1GskIdt8BBt
          8OFpkIjRw2Rdc7BTADjJYbMOJt9dlO9pjGmNvPXGgeYICrwiuhsKCRBoSDc8fe88ads/RO
          lWJeMydnIFnJPu2D1YcFWQCyXQKLPJSBkc8A9i6orLkk2WeNWN6ph4iB03hOPuGknWfSFW
          lGTjNA1hGYACPcY2qA8Cx3TeLitbxyqaE7R+aE/kOBDsy3lL0CBxNwKvqeI9pwrfhevAqJ
          GPFeYIuOvfqrzC5FTdB0y34wpnGT+ol0BQiW3P6e4aLBy3TD6xZ59jOaJFe7qGWMEGtEC+
          dQAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgEnuYyYOlM
          CSR+wvmBY7eKHzFor5ByM7N4F7VZAGKK/vbS3C38xDdiJZwsZUscpe5WspJVCWUTkFxXjn
          GW7vseIfJBVkyqnu2uN8X1j/VDLFESEajcchPhPxtfAMK1/NL99O7rCrYX2pmpkm9tWsFk
          NX5B93sRyDUnHAOkB+zdqU8P0xdzc8kmBl5OOqu1rSjZIgnQjcauEIRIUN+rFuiRRmIvJp
          UvMhkKSsRCH93btGW7A6x5e4iPzP+Em0UFYJdOx2lvu9aVAktQzysGwDN+9c4IC+07UHKT
          UIE5jSbR1QKfavcywNQnCltQ2bTxpnm4A6QHKcdr9Q57dV014FgtmtT/Pw03iyl5MwbEqW
          7YEHSkMyAcd1rjEpOCN2pJjjbrOKLePG0R2ffgvVJnTWGFklCxsJ1/7IASHst1wg1/gu1g
          Kx/TEv+gOKpehAgs2Sz/4kZtFuHO2dbHYC3UrPR5HT8JnQWeCfiT0qwsVQ6xribw0jEYyd
          ZBNWKkPdNocAbA==
          -----END SSH SIGNATURE-----
        SIG
      end

      before do
        key.update!(key: public_key_text)
      end

      it_behaves_like 'verified signature'
    end

    context 'when signed text is an empty string' do
      let(:signed_text) { '' }
      let(:signature_text) do
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
          5Oc3UKCxGsdYuZ/BsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
          AAAAQP2liwaQ44PC9oXf5Xzjq20WLdWEK9nyonvDGtduGUXMOL4yP5A6WvKz7kSt7Vba/U
          MNK0nmnNc7Aokfh/2eRQE=
          -----END SSH SIGNATURE-----
        SIG
      end

      it_behaves_like 'verified signature'
    end

    context 'when signed text is nil' do
      let(:signed_text) { nil }
      let(:signature_text) do
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgko5+o4fR8N175Rr/VI5uRcHUIQ
          MXkzpR8BEylbcXzu4AAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
          OQAAAEC1y2I7o3KqKFlnM+MLkhIo+uRX3YQOYCqycfibyfvmkZTcwqMxgNBInBM9pY3VvS
          sbW2iEdgz34agHbi+1BHIM
          -----END SSH SIGNATURE-----
        SIG
      end

      it_behaves_like 'unverified signature'
    end

    context 'when committer_email is empty' do
      let(:committer_email) { '' }

      it_behaves_like 'unverified signature'
    end

    context 'when committer_email is nil' do
      let(:committer_email) { nil }

      it_behaves_like 'unverified signature'
    end

    context 'when signature_text is empty' do
      let(:signature_text) { '' }

      it_behaves_like 'unverified signature'
    end

    context 'when signature_text is nil' do
      let(:signature_text) { nil }

      it_behaves_like 'unverified signature'
    end

    context 'when user email is not verified' do
      before do
        email = user.emails.find_by(email: committer_email)
        email.update!(confirmed_at: nil)
        user.update!(confirmed_at: nil)
      end

      it 'reports unverified status' do
        expect(signature.verification_status).to eq(:unverified)
      end
    end

    context 'when no user exist with the committer email' do
      before do
        user.delete
      end

      it 'reports other_user status' do
        expect(signature.verification_status).to eq(:other_user)
      end
    end

    context 'when no user exists with the committer email' do
      let(:committer_email) { 'different-email+ssh-commit-test@example.com' }

      it 'reports other_user status' do
        expect(signature.verification_status).to eq(:other_user)
      end
    end

    context 'when signature is invalid' do
      let(:signature_text) do
        # truncated base64
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgko5+o4fR8N175Rr/VI5uRcHUIQ
          MXkzpR8BEylbcXzu4AAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
          OQAAAECQa95KgBkgbMwIPNwHRjHu0WYrKvAc5O/FaBXlTDcPWQHi8WRDhbPNN6MqSYLg/S
          -----END SSH SIGNATURE-----
        SIG
      end

      it_behaves_like 'unverified signature'
    end

    context 'when signature is for a different namespace' do
      let(:signature_text) do
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
          5Oc3UKCxGsdYuZ/BsAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
          OQAAAEAd6Psg4D/5IdSVTy35D4t2iNX4udJnX8JrUCjQl0GoPl1vzPjgyvxdzdoQl6bh1w
          4rror3RuzUYBGzIioIc1MP
          -----END SSH SIGNATURE-----
        SIG
      end

      it_behaves_like 'unverified signature'
    end

    context 'when signature is for a different message' do
      let(:signature_text) do
        <<~SIG
          -----BEGIN SSH SIGNATURE-----
          U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgQtog20+l2pMcPnuoaWXuNpw9u7
          OzPnJzdLUon0+ELNQAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
          OQAAAEB3/B+6c3+XqEuqjiqlVQwQmUdj8WquROtkhdtScEOP8GXcGQx+aaQs5nq4ZJCuu5
          ywcU+4xQaLVpCf7tfGWa4K
          -----END SSH SIGNATURE-----
        SIG
      end

      it_behaves_like 'unverified signature'
    end

    context 'when message has been tampered' do
      let(:signed_text) do
        <<~MSG
          This message was signed by an ssh key
          The pubkey fingerprint is SHA256:RjzeOilYHkiHqz5fefdnrWr8qn5nbroAisuuTMoH9PU
        MSG
      end

      it_behaves_like 'unverified signature'
    end

    context 'when the signing key does not exist in GitLab' do
      context 'when the key is not a signing one' do
        before do
          key.auth!
        end

        it 'reports unknown_key status' do
          expect(signature.verification_status).to eq(:unknown_key)
        end
      end

      context 'when the key is removed' do
        before do
          key.delete
        end

        it 'reports unknown_key status' do
          expect(signature.verification_status).to eq(:unknown_key)
        end
      end
    end

    context 'when key belongs to someone other than the committer' do
      let_it_be(:other_user) { create(:user, email: 'other-user@example.com') }

      let(:committer_email) { other_user.email }

      it 'reports other_user status' do
        expect(signature.verification_status).to eq(:other_user)
      end
    end
  end

  describe '#key_fingerprint' do
    it 'returns the pubkey sha256 fingerprint' do
      expect(signature.key_fingerprint).to eq('dw7gPSvYtkCBU+BbTolbbckUEX3sL6NsGIJTQ4PYEnM')
    end
  end
end