1require 'rubygems/test_case'
2require 'rubygems/commands/cert_command'
3require 'rubygems/fix_openssl_warnings' if RUBY_VERSION < "1.9"
4
5unless defined? OpenSSL then
6  warn "`gem cert` tests are being skipped, module OpenSSL not found"
7end
8
9class TestGemCommandsCertCommand < Gem::TestCase
10
11  ALTERNATE_CERT = load_cert 'alternate'
12
13  ALTERNATE_KEY_FILE = key_path 'alternate'
14  PRIVATE_KEY_FILE   = key_path 'private'
15  PUBLIC_KEY_FILE    = key_path 'public'
16
17  ALTERNATE_CERT_FILE = cert_path 'alternate'
18  CHILD_CERT_FILE     = cert_path 'child'
19  PUBLIC_CERT_FILE    = cert_path 'public'
20
21  def setup
22    super
23
24    @cmd = Gem::Commands::CertCommand.new
25
26    @trust_dir = Gem::Security.trust_dir
27  end
28
29  def test_certificates_matching
30    @trust_dir.trust_cert PUBLIC_CERT
31    @trust_dir.trust_cert ALTERNATE_CERT
32
33    matches = @cmd.certificates_matching ''
34
35    # HACK OpenSSL::X509::Certificate#== is Object#==, so do this the hard way
36    match = matches.next
37    assert_equal ALTERNATE_CERT.to_pem, match.first.to_pem
38    assert_equal @trust_dir.cert_path(ALTERNATE_CERT), match.last
39
40    match = matches.next
41    assert_equal PUBLIC_CERT.to_pem, match.first.to_pem
42    assert_equal @trust_dir.cert_path(PUBLIC_CERT), match.last
43
44    assert_raises StopIteration do
45      matches.next
46    end
47  end
48
49  def test_certificates_matching_filter
50    @trust_dir.trust_cert PUBLIC_CERT
51    @trust_dir.trust_cert ALTERNATE_CERT
52
53    matches = @cmd.certificates_matching 'alternate'
54
55    match = matches.next
56    assert_equal ALTERNATE_CERT.to_pem, match.first.to_pem
57    assert_equal @trust_dir.cert_path(ALTERNATE_CERT), match.last
58
59    assert_raises StopIteration do
60      matches.next
61    end
62  end
63
64  def test_execute_add
65    @cmd.handle_options %W[--add #{PUBLIC_CERT_FILE}]
66
67    use_ui @ui do
68      @cmd.execute
69    end
70
71    cert_path = @trust_dir.cert_path PUBLIC_CERT
72
73    assert_path_exists cert_path
74
75    assert_equal "Added '/CN=nobody/DC=example'\n", @ui.output
76    assert_empty @ui.error
77  end
78
79  def test_execute_add_twice
80    self.class.cert_path 'alternate'
81
82    @cmd.handle_options %W[
83      --add #{PUBLIC_CERT_FILE}
84      --add #{ALTERNATE_CERT_FILE}
85    ]
86
87    use_ui @ui do
88      @cmd.execute
89    end
90
91    expected = <<-EXPECTED
92Added '/CN=nobody/DC=example'
93Added '/CN=alternate/DC=example'
94    EXPECTED
95
96    assert_equal expected, @ui.output
97    assert_empty @ui.error
98  end
99
100  def test_execute_build
101    @cmd.handle_options %W[--build nobody@example.com]
102
103    use_ui @ui do
104      @cmd.execute
105    end
106
107    output = @ui.output.split "\n"
108
109    assert_equal "Certificate: #{File.join @tempdir, 'gem-public_cert.pem'}",
110                 output.shift
111    assert_equal "Private Key: #{File.join @tempdir, 'gem-private_key.pem'}",
112                 output.shift
113
114    assert_equal "Don't forget to move the key file to somewhere private!",
115                 output.shift
116
117    assert_empty output
118    assert_empty @ui.error
119
120    assert_path_exists File.join(@tempdir, 'gem-private_key.pem')
121    assert_path_exists File.join(@tempdir, 'gem-public_cert.pem')
122  end
123
124  def test_execute_build_key
125    @cmd.handle_options %W[
126      --build nobody@example.com
127      --private-key #{PRIVATE_KEY_FILE}
128    ]
129
130    use_ui @ui do
131      @cmd.execute
132    end
133
134    output = @ui.output.split "\n"
135
136    assert_equal "Certificate: #{File.join @tempdir, 'gem-public_cert.pem'}",
137                 output.shift
138    assert_equal "Private Key: #{File.join @tempdir, 'gem-private_key.pem'}",
139                 output.shift
140
141    assert_equal "Don't forget to move the key file to somewhere private!",
142                 output.shift
143
144    assert_empty output
145    assert_empty @ui.error
146
147    assert_path_exists File.join(@tempdir, 'gem-public_cert.pem')
148
149    private_key_file = File.join @tempdir, 'gem-private_key.pem'
150    assert_path_exists private_key_file
151
152    assert_equal PRIVATE_KEY.to_pem, File.read(private_key_file)
153  end
154
155  def test_execute_certificate
156    use_ui @ui do
157      @cmd.handle_options %W[--certificate #{PUBLIC_CERT_FILE}]
158    end
159
160    assert_equal '', @ui.output
161    assert_equal '', @ui.error
162
163    assert_equal PUBLIC_CERT.to_pem, @cmd.options[:issuer_cert].to_pem
164  end
165
166  def test_execute_list
167    @trust_dir.trust_cert PUBLIC_CERT
168    @trust_dir.trust_cert ALTERNATE_CERT
169
170    @cmd.handle_options %W[--list]
171
172    use_ui @ui do
173      @cmd.execute
174    end
175
176    assert_equal "/CN=alternate/DC=example\n/CN=nobody/DC=example\n",
177                 @ui.output
178    assert_empty @ui.error
179  end
180
181  def test_execute_list_filter
182    @trust_dir.trust_cert PUBLIC_CERT
183    @trust_dir.trust_cert ALTERNATE_CERT
184
185    @cmd.handle_options %W[--list nobody]
186
187    use_ui @ui do
188      @cmd.execute
189    end
190
191    assert_equal "/CN=nobody/DC=example\n", @ui.output
192    assert_empty @ui.error
193  end
194
195  def test_execute_private_key
196    use_ui @ui do
197      @cmd.send :handle_options, %W[--private-key #{PRIVATE_KEY_FILE}]
198    end
199
200    assert_equal '', @ui.output
201    assert_equal '', @ui.error
202
203    assert_equal PRIVATE_KEY.to_pem, @cmd.options[:key].to_pem
204  end
205
206  def test_execute_remove
207    @trust_dir.trust_cert PUBLIC_CERT
208
209    cert_path = @trust_dir.cert_path PUBLIC_CERT
210
211    assert_path_exists cert_path
212
213    @cmd.handle_options %W[--remove nobody]
214
215    use_ui @ui do
216      @cmd.execute
217    end
218
219    assert_equal "Removed '/CN=nobody/DC=example'\n", @ui.output
220    assert_equal '', @ui.error
221
222    refute_path_exists cert_path
223  end
224
225  def test_execute_remove_multiple
226    @trust_dir.trust_cert PUBLIC_CERT
227    @trust_dir.trust_cert ALTERNATE_CERT
228
229    public_path = @trust_dir.cert_path PUBLIC_CERT
230    alternate_path = @trust_dir.cert_path ALTERNATE_CERT
231
232    assert_path_exists public_path
233    assert_path_exists alternate_path
234
235    @cmd.handle_options %W[--remove example]
236
237    use_ui @ui do
238      @cmd.execute
239    end
240
241    expected = <<-EXPECTED
242Removed '/CN=alternate/DC=example'
243Removed '/CN=nobody/DC=example'
244    EXPECTED
245
246    assert_equal expected, @ui.output
247    assert_equal '', @ui.error
248
249    refute_path_exists public_path
250    refute_path_exists alternate_path
251  end
252
253  def test_execute_remove_twice
254    @trust_dir.trust_cert PUBLIC_CERT
255    @trust_dir.trust_cert ALTERNATE_CERT
256
257    public_path = @trust_dir.cert_path PUBLIC_CERT
258    alternate_path = @trust_dir.cert_path ALTERNATE_CERT
259
260    assert_path_exists public_path
261    assert_path_exists alternate_path
262
263    @cmd.handle_options %W[--remove nobody --remove alternate]
264
265    use_ui @ui do
266      @cmd.execute
267    end
268
269    expected = <<-EXPECTED
270Removed '/CN=nobody/DC=example'
271Removed '/CN=alternate/DC=example'
272    EXPECTED
273
274    assert_equal expected, @ui.output
275    assert_equal '', @ui.error
276
277    refute_path_exists public_path
278    refute_path_exists alternate_path
279  end
280
281  def test_execute_sign
282    path = File.join @tempdir, 'cert.pem'
283    Gem::Security.write ALTERNATE_CERT, path, 0600
284
285    assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s
286
287    @cmd.handle_options %W[
288      --private-key #{PRIVATE_KEY_FILE}
289      --certificate #{PUBLIC_CERT_FILE}
290
291      --sign #{path}
292    ]
293
294    use_ui @ui do
295      @cmd.execute
296    end
297
298    assert_equal '', @ui.output
299    assert_equal '', @ui.error
300
301    cert = OpenSSL::X509::Certificate.new File.read path
302
303    assert_equal '/CN=nobody/DC=example', cert.issuer.to_s
304
305    mask = 0100600 & (~File.umask)
306
307    assert_equal mask, File.stat(path).mode unless win_platform?
308  end
309
310  def test_execute_sign_default
311    FileUtils.mkdir_p File.join Gem.user_home, '.gem'
312
313    private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem'
314    Gem::Security.write PRIVATE_KEY, private_key_path
315
316    public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem'
317    Gem::Security.write PUBLIC_CERT, public_cert_path
318
319    path = File.join @tempdir, 'cert.pem'
320    Gem::Security.write ALTERNATE_CERT, path, 0600
321
322    assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s
323
324    @cmd.handle_options %W[--sign #{path}]
325
326    use_ui @ui do
327      @cmd.execute
328    end
329
330    assert_equal '', @ui.output
331    assert_equal '', @ui.error
332
333    cert = OpenSSL::X509::Certificate.new File.read path
334
335    assert_equal '/CN=nobody/DC=example', cert.issuer.to_s
336
337    mask = 0100600 & (~File.umask)
338
339    assert_equal mask, File.stat(path).mode unless win_platform?
340  end
341
342  def test_execute_sign_no_cert
343    FileUtils.mkdir_p File.join Gem.user_home, '.gem'
344
345    private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem'
346    Gem::Security.write PRIVATE_KEY, private_key_path
347
348    path = File.join @tempdir, 'cert.pem'
349    Gem::Security.write ALTERNATE_CERT, path, 0600
350
351    assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s
352
353    @cmd.handle_options %W[--sign #{path}]
354
355    use_ui @ui do
356      assert_raises Gem::MockGemUi::TermError do
357        @cmd.execute
358      end
359    end
360
361    assert_equal '', @ui.output
362
363    expected = <<-EXPECTED
364ERROR:  --certificate not specified and ~/.gem/gem-public_cert.pem does not exist
365    EXPECTED
366
367    assert_equal expected, @ui.error
368  end
369
370  def test_execute_sign_no_key
371    FileUtils.mkdir_p File.join Gem.user_home, '.gem'
372
373    public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem'
374    Gem::Security.write PUBLIC_CERT, public_cert_path
375
376    path = File.join @tempdir, 'cert.pem'
377    Gem::Security.write ALTERNATE_CERT, path, 0600
378
379    assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s
380
381    @cmd.handle_options %W[--sign #{path}]
382
383    use_ui @ui do
384      assert_raises Gem::MockGemUi::TermError do
385        @cmd.execute
386      end
387    end
388
389    assert_equal '', @ui.output
390
391    expected = <<-EXPECTED
392ERROR:  --private-key not specified and ~/.gem/gem-private_key.pem does not exist
393    EXPECTED
394
395    assert_equal expected, @ui.error
396  end
397
398  def test_handle_options
399    @cmd.handle_options %W[
400      --add #{PUBLIC_CERT_FILE}
401      --add #{ALTERNATE_CERT_FILE}
402
403      --remove nobody
404      --remove example
405
406      --list
407      --list example
408
409      --build nobody@example
410      --build other@example
411    ]
412
413    assert_equal [PUBLIC_CERT.to_pem, ALTERNATE_CERT.to_pem],
414                 @cmd.options[:add].map { |cert| cert.to_pem }
415
416    assert_equal %w[nobody example], @cmd.options[:remove]
417
418    assert_equal %w[nobody@example other@example],
419                 @cmd.options[:build].map { |name| name.to_s }
420
421    assert_equal ['', 'example'], @cmd.options[:list]
422  end
423
424  def test_handle_options_add_bad
425    nonexistent = File.join @tempdir, 'nonexistent'
426    e = assert_raises OptionParser::InvalidArgument do
427      @cmd.handle_options %W[--add #{nonexistent}]
428    end
429
430    assert_equal "invalid argument: --add #{nonexistent}: does not exist",
431                 e.message
432
433    bad = File.join @tempdir, 'bad'
434    FileUtils.touch bad
435
436    e = assert_raises OptionParser::InvalidArgument do
437      @cmd.handle_options %W[--add #{bad}]
438    end
439
440    assert_equal "invalid argument: --add #{bad}: invalid X509 certificate",
441                 e.message
442  end
443
444  def test_handle_options_certificate
445    nonexistent = File.join @tempdir, 'nonexistent'
446    e = assert_raises OptionParser::InvalidArgument do
447      @cmd.handle_options %W[--certificate #{nonexistent}]
448    end
449
450    assert_equal "invalid argument: --certificate #{nonexistent}: does not exist",
451                 e.message
452
453    bad = File.join @tempdir, 'bad'
454    FileUtils.touch bad
455
456    e = assert_raises OptionParser::InvalidArgument do
457      @cmd.handle_options %W[--certificate #{bad}]
458    end
459
460    assert_equal "invalid argument: " +
461                 "--certificate #{bad}: invalid X509 certificate",
462                 e.message
463  end
464
465  def test_handle_options_key_bad
466    nonexistent = File.join @tempdir, 'nonexistent'
467    e = assert_raises OptionParser::InvalidArgument do
468      @cmd.handle_options %W[--private-key #{nonexistent}]
469    end
470
471    assert_equal "invalid argument: " +
472                 "--private-key #{nonexistent}: does not exist",
473                 e.message
474
475    bad = File.join @tempdir, 'bad'
476    FileUtils.touch bad
477
478    e = assert_raises OptionParser::InvalidArgument do
479      @cmd.handle_options %W[--private-key #{bad}]
480    end
481
482    assert_equal "invalid argument: --private-key #{bad}: invalid RSA key",
483                 e.message
484
485    e = assert_raises OptionParser::InvalidArgument do
486      @cmd.handle_options %W[--private-key #{PUBLIC_KEY_FILE}]
487    end
488
489    assert_equal "invalid argument: " +
490                 "--private-key #{PUBLIC_KEY_FILE}: private key not found",
491                 e.message
492  end
493
494  def test_handle_options_sign
495    @cmd.handle_options %W[
496      --private-key #{ALTERNATE_KEY_FILE}
497      --private-key #{PRIVATE_KEY_FILE}
498
499      --certificate #{ALTERNATE_CERT_FILE}
500      --certificate #{PUBLIC_CERT_FILE}
501
502      --sign #{ALTERNATE_CERT_FILE}
503      --sign #{CHILD_CERT_FILE}
504    ]
505
506    assert_equal PRIVATE_KEY.to_pem, @cmd.options[:key].to_pem
507    assert_equal PUBLIC_CERT.to_pem, @cmd.options[:issuer_cert].to_pem
508
509    assert_equal [ALTERNATE_CERT_FILE, CHILD_CERT_FILE], @cmd.options[:sign]
510  end
511
512  def test_handle_options_sign_nonexistent
513    nonexistent = File.join @tempdir, 'nonexistent'
514    e = assert_raises OptionParser::InvalidArgument do
515      @cmd.handle_options %W[
516        --private-key #{ALTERNATE_KEY_FILE}
517
518        --certificate #{ALTERNATE_CERT_FILE}
519
520        --sign #{nonexistent}
521      ]
522    end
523
524    assert_equal "invalid argument: --sign #{nonexistent}: does not exist",
525                 e.message
526  end
527
528end if defined? OpenSSL
529
530