1# coding: UTF-8
2
3require 'rubygems/test_case'
4
5class TestGemSecurityPolicy < Gem::TestCase
6
7  ALTERNATE_KEY    = load_key 'alternate'
8  INVALID_KEY      = load_key 'invalid'
9  CHILD_KEY        = load_key 'child'
10  GRANDCHILD_KEY   = load_key 'grandchild'
11  INVALIDCHILD_KEY = load_key 'invalidchild'
12
13  ALTERNATE_CERT      = load_cert 'alternate'
14  CHILD_CERT          = load_cert 'child'
15  EXPIRED_CERT        = load_cert 'expired'
16  FUTURE_CERT         = load_cert 'future'
17  GRANDCHILD_CERT     = load_cert 'grandchild'
18  INVALIDCHILD_CERT   = load_cert 'invalidchild'
19  INVALID_ISSUER_CERT = load_cert 'invalid_issuer'
20  INVALID_SIGNER_CERT = load_cert 'invalid_signer'
21  WRONG_KEY_CERT      = load_cert 'wrong_key'
22
23  def setup
24    super
25
26    @spec = quick_gem 'a' do |s|
27      s.description = 'π'
28      s.files = %w[lib/code.rb]
29    end
30
31    @sha1 = OpenSSL::Digest::SHA1
32    @trust_dir = Gem::Security.trust_dir.dir # HACK use the object
33
34    @no        = Gem::Security::NoSecurity
35    @almost_no = Gem::Security::AlmostNoSecurity
36    @low       = Gem::Security::LowSecurity
37    @medium    = Gem::Security::MediumSecurity
38    @high      = Gem::Security::HighSecurity
39
40    @chain = Gem::Security::Policy.new(
41      'Chain',
42      :verify_data   => true,
43      :verify_signer => true,
44      :verify_chain  => true,
45      :verify_root   => false,
46      :only_trusted  => false,
47      :only_signed   => false
48    )
49
50    @root = Gem::Security::Policy.new(
51      'Root',
52      :verify_data   => true,
53      :verify_signer => true,
54      :verify_chain  => true,
55      :verify_root   => true,
56      :only_trusted  => false,
57      :only_signed   => false
58    )
59  end
60
61  def test_check_data
62    data = digest 'hello'
63
64    signature = sign data
65
66    assert @almost_no.check_data(PUBLIC_KEY, @sha1, signature, data)
67  end
68
69  def test_check_data_invalid
70    data = digest 'hello'
71
72    signature = sign data
73
74    invalid = digest 'hello!'
75
76    e = assert_raises Gem::Security::Exception do
77      @almost_no.check_data PUBLIC_KEY, @sha1, signature, invalid
78    end
79
80    assert_equal 'invalid signature', e.message
81  end
82
83  def test_check_chain
84    chain = [PUBLIC_CERT, CHILD_CERT, GRANDCHILD_CERT]
85
86    assert @chain.check_chain chain, Time.now
87  end
88
89  def test_check_chain_empty_chain
90    e = assert_raises Gem::Security::Exception do
91      @chain.check_chain [], Time.now
92    end
93
94    assert_equal 'empty signing chain', e.message
95  end
96
97  def test_check_chain_invalid
98    chain = [PUBLIC_CERT, CHILD_CERT, INVALIDCHILD_CERT]
99
100    e = assert_raises Gem::Security::Exception do
101      @chain.check_chain chain, Time.now
102    end
103
104    assert_equal "invalid signing chain: " +
105                 "certificate #{INVALIDCHILD_CERT.subject} " +
106                 "was not issued by #{CHILD_CERT.subject}", e.message
107  end
108
109  def test_check_chain_no_chain
110    e = assert_raises Gem::Security::Exception do
111      @chain.check_chain nil, Time.now
112    end
113
114    assert_equal 'missing signing chain', e.message
115  end
116
117  def test_check_cert
118    assert @low.check_cert(PUBLIC_CERT, nil, Time.now)
119  end
120
121  def test_check_cert_expired
122    e = assert_raises Gem::Security::Exception do
123      @low.check_cert EXPIRED_CERT, nil, Time.now
124    end
125
126    assert_equal "certificate #{EXPIRED_CERT.subject} " +
127                 "not valid after #{EXPIRED_CERT.not_after}",
128                 e.message
129  end
130
131  def test_check_cert_future
132    e = assert_raises Gem::Security::Exception do
133      @low.check_cert FUTURE_CERT, nil, Time.now
134    end
135
136    assert_equal "certificate #{FUTURE_CERT.subject} " +
137                 "not valid before #{FUTURE_CERT.not_before}",
138                 e.message
139  end
140
141  def test_check_cert_invalid_issuer
142    e = assert_raises Gem::Security::Exception do
143      @low.check_cert INVALID_ISSUER_CERT, PUBLIC_CERT, Time.now
144    end
145
146    assert_equal "certificate #{INVALID_ISSUER_CERT.subject} " +
147                 "was not issued by #{PUBLIC_CERT.subject}",
148                 e.message
149  end
150
151  def test_check_cert_issuer
152    assert @low.check_cert(CHILD_CERT, PUBLIC_CERT, Time.now)
153  end
154
155  def test_check_cert_no_signer
156    e = assert_raises Gem::Security::Exception do
157      @high.check_cert(nil, nil, Time.now)
158    end
159
160    assert_equal 'missing signing certificate', e.message
161  end
162
163  def test_check_key
164    assert @almost_no.check_key(PUBLIC_CERT, PRIVATE_KEY)
165  end
166
167  def test_check_key_no_signer
168    assert @almost_no.check_key(nil, nil)
169
170    e = assert_raises Gem::Security::Exception do
171      @high.check_key(nil, nil)
172    end
173
174    assert_equal 'missing key or signature', e.message
175  end
176
177  def test_check_key_wrong_key
178    e = assert_raises Gem::Security::Exception do
179      @almost_no.check_key(PUBLIC_CERT, ALTERNATE_KEY)
180    end
181
182    assert_equal "certificate #{PUBLIC_CERT.subject} " +
183                 "does not match the signing key", e.message
184  end
185
186  def test_check_root
187    chain = [PUBLIC_CERT, CHILD_CERT, INVALIDCHILD_CERT]
188
189    assert @chain.check_root chain, Time.now
190  end
191
192  def test_check_root_empty_chain
193    e = assert_raises Gem::Security::Exception do
194      @chain.check_root [], Time.now
195    end
196
197    assert_equal 'missing root certificate', e.message
198  end
199
200  def test_check_root_invalid_signer
201    chain = [INVALID_SIGNER_CERT]
202
203    e = assert_raises Gem::Security::Exception do
204      @chain.check_root chain, Time.now
205    end
206
207    assert_equal "certificate #{INVALID_SIGNER_CERT.subject} " +
208                 "was not issued by #{INVALID_SIGNER_CERT.issuer}",
209                 e.message
210  end
211
212  def test_check_root_not_self_signed
213    chain = [INVALID_ISSUER_CERT]
214
215    e = assert_raises Gem::Security::Exception do
216      @chain.check_root chain, Time.now
217    end
218
219    assert_equal "root certificate #{INVALID_ISSUER_CERT.subject} " +
220                 "is not self-signed (issuer #{INVALID_ISSUER_CERT.issuer})",
221                 e.message
222  end
223
224  def test_check_root_no_chain
225    e = assert_raises Gem::Security::Exception do
226      @chain.check_root nil, Time.now
227    end
228
229    assert_equal 'missing signing chain', e.message
230  end
231
232  def test_check_trust
233    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
234
235    assert @high.check_trust [PUBLIC_CERT], @sha1, @trust_dir
236  end
237
238  def test_check_trust_child
239    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
240
241    assert @high.check_trust [PUBLIC_CERT, CHILD_CERT], @sha1, @trust_dir
242  end
243
244  def test_check_trust_empty_chain
245    e = assert_raises Gem::Security::Exception do
246      @chain.check_trust [], @sha1, @trust_dir
247    end
248
249    assert_equal 'missing root certificate', e.message
250  end
251
252  def test_check_trust_mismatch
253    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
254
255    e = assert_raises Gem::Security::Exception do
256      @high.check_trust [WRONG_KEY_CERT], @sha1, @trust_dir
257    end
258
259    assert_equal "trusted root certificate #{PUBLIC_CERT.subject} checksum " +
260                 "does not match signing root certificate checksum", e.message
261  end
262
263  def test_check_trust_no_chain
264    e = assert_raises Gem::Security::Exception do
265      @chain.check_trust nil, @sha1, @trust_dir
266    end
267
268    assert_equal 'missing signing chain', e.message
269  end
270
271  def test_check_trust_no_trust
272    e = assert_raises Gem::Security::Exception do
273      @high.check_trust [PUBLIC_CERT], @sha1, @trust_dir
274    end
275
276    assert_equal "root cert #{PUBLIC_CERT.subject} is not trusted", e.message
277  end
278
279  def test_check_trust_no_trust_child
280    e = assert_raises Gem::Security::Exception do
281      @high.check_trust [PUBLIC_CERT, CHILD_CERT], @sha1, @trust_dir
282    end
283
284    assert_equal "root cert #{PUBLIC_CERT.subject} is not trusted " +
285                 "(root of signing cert #{CHILD_CERT.subject})", e.message
286  end
287
288  def test_verify
289    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
290
291    assert @almost_no.verify [PUBLIC_CERT], nil, *dummy_signatures
292  end
293
294  def test_verify_chain_signatures
295    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
296
297    assert @high.verify [PUBLIC_CERT], nil, *dummy_signatures
298  end
299
300  def test_verify_chain_key
301    @almost_no.verify [PUBLIC_CERT], PRIVATE_KEY, *dummy_signatures
302  end
303
304  def test_verify_no_digests
305    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
306
307    _, signatures = dummy_signatures
308
309    e = assert_raises Gem::Security::Exception do
310      @almost_no.verify [PUBLIC_CERT], nil, {}, signatures
311    end
312
313    assert_equal 'no digests provided (probable bug)', e.message
314  end
315
316  def test_verify_no_digests_no_security
317    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
318
319    _, signatures = dummy_signatures
320
321    e = assert_raises Gem::Security::Exception do
322      @no.verify [PUBLIC_CERT], nil, {}, signatures
323    end
324
325    assert_equal 'missing digest for 0', e.message
326  end
327
328  def test_verify_not_enough_signatures
329    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
330
331    digests, signatures = dummy_signatures
332
333    data = digest 'goodbye'
334
335    signatures[1] = PRIVATE_KEY.sign @sha1.new, data.digest
336
337    e = assert_raises Gem::Security::Exception do
338      @almost_no.verify [PUBLIC_CERT], nil, digests, signatures
339    end
340
341    assert_equal 'missing digest for 1', e.message
342  end
343
344  def test_verify_wrong_digest_type
345    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
346
347    sha512 = OpenSSL::Digest::SHA512
348
349    data = sha512.new
350    data << 'hello'
351
352    digests    = { 'SHA512' => { 0 => data } }
353    signature  = PRIVATE_KEY.sign sha512.new, data.digest
354    signatures = { 0 => signature }
355
356    e = assert_raises Gem::Security::Exception do
357      @almost_no.verify [PUBLIC_CERT], nil, digests, signatures
358    end
359
360    assert_equal 'no digests provided (probable bug)', e.message
361  end
362
363  def test_verify_signatures_chain
364    @spec.cert_chain = [PUBLIC_CERT, CHILD_CERT]
365
366    assert @chain.verify_signatures @spec, *dummy_signatures(CHILD_KEY)
367  end
368
369  def test_verify_signatures_data
370    @spec.cert_chain = [PUBLIC_CERT]
371
372    @almost_no.verify_signatures @spec, *dummy_signatures
373  end
374
375  def test_verify_signatures_root
376    @spec.cert_chain = [PUBLIC_CERT, CHILD_CERT]
377
378    assert @root.verify_signatures @spec, *dummy_signatures(CHILD_KEY)
379  end
380
381  def test_verify_signatures_signer
382    @spec.cert_chain = [PUBLIC_CERT]
383
384    assert @low.verify_signatures @spec, *dummy_signatures
385  end
386
387  def test_verify_signatures_trust
388    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
389
390    @spec.cert_chain = [PUBLIC_CERT]
391
392    assert @high.verify_signatures @spec, *dummy_signatures
393  end
394
395  def test_verify_signatures
396    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
397
398    @spec.cert_chain = [PUBLIC_CERT.to_s]
399
400    metadata_gz = Gem.gzip @spec.to_yaml
401
402    package = Gem::Package.new 'nonexistent.gem'
403    package.checksums['SHA1'] = {}
404
405    s = StringIO.new metadata_gz
406    def s.full_name() 'metadata.gz' end
407
408    digests = package.digest s
409    metadata_gz_digest = digests['SHA1']['metadata.gz']
410
411    signatures = {}
412    signatures['metadata.gz'] =
413      PRIVATE_KEY.sign @sha1.new, metadata_gz_digest.digest
414
415    assert @high.verify_signatures @spec, digests, signatures
416  end
417
418  def test_verify_signatures_missing
419    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
420
421    @spec.cert_chain = [PUBLIC_CERT.to_s]
422
423    metadata_gz = Gem.gzip @spec.to_yaml
424
425    package = Gem::Package.new 'nonexistent.gem'
426    package.checksums['SHA1'] = {}
427
428    s = StringIO.new metadata_gz
429    def s.full_name() 'metadata.gz' end
430
431    digests = package.digest s
432    digests['SHA1']['data.tar.gz'] = OpenSSL::Digest.new 'SHA1', 'hello'
433
434    metadata_gz_digest = digests['SHA1']['metadata.gz']
435
436    signatures = {}
437    signatures['metadata.gz'] =
438      PRIVATE_KEY.sign @sha1.new, metadata_gz_digest.digest
439
440    e = assert_raises Gem::Security::Exception do
441      @high.verify_signatures @spec, digests, signatures
442    end
443
444    assert_equal 'missing signature for data.tar.gz', e.message
445  end
446
447  def test_verify_signatures_none
448    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
449
450    @spec.cert_chain = [PUBLIC_CERT.to_s]
451
452    metadata_gz = Gem.gzip @spec.to_yaml
453
454    package = Gem::Package.new 'nonexistent.gem'
455    package.checksums['SHA1'] = {}
456
457    s = StringIO.new metadata_gz
458    def s.full_name() 'metadata.gz' end
459
460    digests = package.digest s
461    digests['SHA1']['data.tar.gz'] = OpenSSL::Digest.new 'SHA1', 'hello'
462
463    assert_raises Gem::Security::Exception do
464      @almost_no.verify_signatures @spec, digests, {}
465    end
466  end
467
468  def digest data
469    digester = @sha1.new
470    digester << data
471    digester
472  end
473
474  def sign data, key = PRIVATE_KEY
475    key.sign @sha1.new, data.digest
476  end
477
478  def dummy_signatures key = PRIVATE_KEY
479    data = digest 'hello'
480
481    digests    = { 'SHA1' => { 0 => data } }
482    signatures = { 0 => sign(data, key) }
483
484    return digests, signatures
485  end
486
487end
488
489