rubygems-update 0.8.10 → 0.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rubygems-update might be problematic. Click here for more details.

Files changed (51) hide show
  1. data/ChangeLog +66 -0
  2. data/README +17 -3
  3. data/Rakefile +29 -11
  4. data/bin/gem_mirror +67 -0
  5. data/examples/application/an-app.gemspec +2 -0
  6. data/lib/rubygems.rb +18 -3
  7. data/lib/rubygems/builder.rb +14 -1
  8. data/lib/rubygems/cmd_manager.rb +2 -0
  9. data/lib/rubygems/command.rb +26 -2
  10. data/lib/rubygems/custom_require.rb +30 -21
  11. data/lib/rubygems/format.rb +4 -4
  12. data/lib/rubygems/gem_commands.rb +161 -9
  13. data/lib/rubygems/gem_openssl.rb +34 -0
  14. data/lib/rubygems/gem_runner.rb +5 -1
  15. data/lib/rubygems/installer.rb +117 -38
  16. data/lib/rubygems/package.rb +135 -25
  17. data/lib/rubygems/remote_installer.rb +59 -29
  18. data/lib/rubygems/rubygems_version.rb +1 -1
  19. data/lib/rubygems/security.rb +478 -0
  20. data/lib/rubygems/specification.rb +48 -28
  21. data/post-install.rb +2 -1
  22. data/scripts/gemdoc.rb +2 -2
  23. data/scripts/specdoc.rb +25 -24
  24. data/scripts/upload_gemdoc.rb +134 -0
  25. data/setup.rb +1 -1
  26. data/test/data/a-0.0.1.gem +0 -0
  27. data/test/data/a-0.0.2.gem +0 -0
  28. data/test/data/b-0.0.2.gem +0 -0
  29. data/test/data/c-1.2.gem +0 -0
  30. data/test/data/gemhome/cache/a-0.0.1.gem +0 -0
  31. data/test/data/gemhome/cache/a-0.0.2.gem +0 -0
  32. data/test/data/gemhome/cache/b-0.0.2.gem +0 -0
  33. data/test/data/gemhome/cache/c-1.2.gem +0 -0
  34. data/test/data/gemhome/specifications/a-0.0.1.gemspec +1 -1
  35. data/test/data/gemhome/specifications/a-0.0.2.gemspec +1 -1
  36. data/test/data/gemhome/specifications/b-0.0.2.gemspec +1 -1
  37. data/test/data/gemhome/specifications/c-1.2.gemspec +1 -1
  38. data/test/data/one/one-0.0.1.gem +0 -0
  39. data/test/fake_certlib/openssl.rb +1 -0
  40. data/test/functional.rb +49 -14
  41. data/test/gemutilities.rb +69 -5
  42. data/test/test_cached_fetcher.rb +5 -7
  43. data/test/test_file_list.rb +96 -0
  44. data/test/test_gempaths.rb +36 -34
  45. data/test/test_installer.rb +214 -0
  46. data/test/test_local_cache.rb +45 -102
  47. data/test/test_parse_commands.rb +3 -1
  48. data/test/test_remote_installer.rb +24 -5
  49. data/test/test_specific_extras.rb +40 -0
  50. data/test/test_specification.rb +106 -16
  51. metadata +14 -3
@@ -13,6 +13,7 @@ require 'find'
13
13
  require 'stringio'
14
14
 
15
15
  require 'rubygems/specification'
16
+ require 'rubygems/security'
16
17
 
17
18
  module Gem
18
19
 
@@ -470,10 +471,13 @@ class TarInput
470
471
  attr_reader :metadata
471
472
  class << self; private :new end
472
473
 
473
- def initialize(io)
474
+ def initialize(io, security_policy = nil)
474
475
  @io = io
475
- @tarreader = TarReader.new @io
476
+ @tarreader = TarReader.new(@io)
476
477
  has_meta = false
478
+ data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil
479
+ dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil
480
+
477
481
  @tarreader.each do |entry|
478
482
  case entry.full_name
479
483
  when "metadata"
@@ -483,7 +487,17 @@ class TarInput
483
487
  break
484
488
  when "metadata.gz"
485
489
  begin
486
- gzis = Zlib::GzipReader.new entry
490
+ # if we have a security_policy, then pre-read the
491
+ # metadata file and calculate it's digest
492
+ sio = nil
493
+ if security_policy
494
+ Gem.ensure_ssl_available
495
+ sio = StringIO.new(entry.read)
496
+ meta_dgst = dgst_algo.digest(sio.string)
497
+ sio.rewind
498
+ end
499
+
500
+ gzis = Zlib::GzipReader.new(sio || entry)
487
501
  # YAML wants an instance of IO
488
502
  # (GS) Changed to line below: @metadata = YAML.load(gzis) rescue nil
489
503
  @metadata = load_gemspec(gzis)
@@ -491,8 +505,59 @@ class TarInput
491
505
  ensure
492
506
  gzis.close
493
507
  end
508
+ when 'metadata.gz.sig'
509
+ Gem.ensure_ssl_available
510
+ meta_sig = entry.read
511
+ when 'data.tar.gz.sig'
512
+ Gem.ensure_ssl_available
513
+ data_sig = entry.read
514
+ when 'data.tar.gz'
515
+ if security_policy
516
+ Gem.ensure_ssl_available
517
+ data_dgst = dgst_algo.digest(entry.read)
518
+ end
494
519
  end
495
520
  end
521
+
522
+ if security_policy
523
+ Gem.ensure_ssl_available
524
+ # map trust policy from string to actual class (or a
525
+ # serialized YAML file, if that exists)
526
+ if (security_policy.is_a?(String))
527
+ if Gem::Security.constants.index(security_policy)
528
+ # load one of the pre-defined security policies
529
+ security_policy = Gem::Security.const_get(security_policy)
530
+ elsif File.exists?(security_policy)
531
+ # FIXME: this doesn't work yet
532
+ security_policy = YAML::load(File.read(security_policy))
533
+ else
534
+ raise Gem::Exception, "Unknown trust policy '#{security_policy}'"
535
+ end
536
+ end
537
+
538
+ if data_sig && data_dgst && meta_sig && meta_dgst
539
+ # the user has a trust policy, and we have a signed gem
540
+ # file, so use the trust policy to verify the gem signature
541
+
542
+ begin
543
+ security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain)
544
+ rescue Exception => e
545
+ raise "Couldn't verify data signature: #{e}"
546
+ end
547
+
548
+ begin
549
+ security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain)
550
+ rescue Exception => e
551
+ raise "Couldn't verify metadata signature: #{e}"
552
+ end
553
+ elsif security_policy.only_signed
554
+ raise Gem::Exception, "Unsigned gem"
555
+ else
556
+ # FIXME: should display warning here (trust policy, but
557
+ # either unsigned or badly signed gem file)
558
+ end
559
+ end
560
+
496
561
  @tarreader.rewind
497
562
  @fileops = FileOperations.new
498
563
  raise RuntimeError, "No metadata found!" unless has_meta
@@ -505,14 +570,14 @@ class TarInput
505
570
  nil
506
571
  end
507
572
 
508
- def self.open(filename, &block)
509
- open_from_io(File.open(filename, "rb"), &block)
573
+ def self.open(filename, security_policy = nil, &block)
574
+ open_from_io(File.open(filename, "rb"), security_policy, &block)
510
575
  end
511
576
 
512
- def self.open_from_io(io, &block)
577
+ def self.open_from_io(io, security_policy = nil, &block)
513
578
  raise "Want a block" unless block_given?
514
579
  begin
515
- is = new(io)
580
+ is = new(io, security_policy)
516
581
  yield is
517
582
  ensure
518
583
  is.close if is
@@ -522,7 +587,7 @@ class TarInput
522
587
  def each(&block)
523
588
  @tarreader.each do |entry|
524
589
  next unless entry.full_name == "data.tar.gz"
525
- is = zipped_stream(entry)
590
+ is = zipped_stream(entry)
526
591
  begin
527
592
  TarReader.new(is) do |inner|
528
593
  inner.each(&block)
@@ -543,11 +608,11 @@ class TarInput
543
608
  # later.
544
609
  def zipped_stream(entry)
545
610
  if Zlib::ZLIB_VERSION < '1.2.1'
546
- zis = Zlib::GzipReader.new entry
547
- dis = zis.read
548
- is = StringIO.new(dis)
611
+ zis = Zlib::GzipReader.new entry
612
+ dis = zis.read
613
+ is = StringIO.new(dis)
549
614
  else
550
- is = Zlib::GzipReader.new entry
615
+ is = Zlib::GzipReader.new entry
551
616
  end
552
617
  ensure
553
618
  zis.finish if zis
@@ -617,21 +682,25 @@ class TarOutput
617
682
  @external
618
683
  end
619
684
 
620
- def self.open(filename, &block)
685
+ def self.open(filename, signer = nil, &block)
621
686
  io = File.open(filename, "wb")
622
- open_from_io(io, &block)
687
+ open_from_io(io, signer, &block)
623
688
  nil
624
689
  end
625
690
 
626
- def self.open_from_io(io, &block)
691
+ def self.open_from_io(io, signer = nil, &block)
627
692
  outputter = new(io)
628
693
  metadata = nil
629
694
  set_meta = lambda{|x| metadata = x}
630
695
  raise "Want a block" unless block_given?
631
696
  begin
697
+ data_sig, meta_sig = nil, nil
698
+
632
699
  outputter.external_handle.add_file("data.tar.gz", 0644) do |inner|
633
700
  begin
634
- os = Zlib::GzipWriter.new inner
701
+ sio = signer ? StringIO.new : nil
702
+ os = Zlib::GzipWriter.new(sio || inner)
703
+
635
704
  TarWriter.new(os) do |inner_tar_stream|
636
705
  klass = class <<inner_tar_stream; self end
637
706
  klass.send(:define_method, :metadata=, &set_meta)
@@ -641,17 +710,56 @@ class TarOutput
641
710
  os.flush
642
711
  os.finish
643
712
  #os.close
713
+
714
+ # if we have a signing key, then sign the data
715
+ # digest and return the signature
716
+ data_sig = nil
717
+ if signer
718
+ dgst_algo = Gem::Security::OPT[:dgst_algo]
719
+ dig = dgst_algo.digest(sio.string)
720
+ data_sig = signer.sign(dig)
721
+ inner.write(sio.string)
722
+ end
644
723
  end
645
724
  end
725
+
726
+ # if we have a data signature, then write it to the gem too
727
+ if data_sig
728
+ sig_file = 'data.tar.gz.sig'
729
+ outputter.external_handle.add_file(sig_file, 0644) do |os|
730
+ os.write(data_sig)
731
+ end
732
+ end
733
+
646
734
  outputter.external_handle.add_file("metadata.gz", 0644) do |os|
647
735
  begin
648
- gzos = Zlib::GzipWriter.new os
736
+ sio = signer ? StringIO.new : nil
737
+ gzos = Zlib::GzipWriter.new(sio || os)
649
738
  gzos.write metadata
650
739
  ensure
651
740
  gzos.flush
652
741
  gzos.finish
742
+
743
+ # if we have a signing key, then sign the metadata
744
+ # digest and return the signature
745
+ if signer
746
+ dgst_algo = Gem::Security::OPT[:dgst_algo]
747
+ dig = dgst_algo.digest(sio.string)
748
+ meta_sig = signer.sign(dig)
749
+ os.write(sio.string)
750
+ end
653
751
  end
654
752
  end
753
+
754
+ # if we have a metadata signature, then write to the gem as
755
+ # well
756
+ if meta_sig
757
+ sig_file = 'metadata.gz.sig'
758
+ outputter.external_handle.add_file(sig_file, 0644) do |os|
759
+ os.write(meta_sig)
760
+ end
761
+ end
762
+
655
763
  ensure
656
764
  outputter.close
657
765
  end
@@ -668,34 +776,36 @@ end # module Package
668
776
 
669
777
  module Package
670
778
  #FIXME: refactor the following 2 methods
671
- def self.open(dest, mode = "r", &block)
779
+ def self.open(dest, mode = "r", signer = nil, &block)
672
780
  raise "Block needed" unless block_given?
673
781
 
674
782
  case mode
675
783
  when "r"
676
- TarInput.open(dest, &block)
784
+ security_policy = signer
785
+ TarInput.open(dest, security_policy, &block)
677
786
  when "w"
678
- TarOutput.open(dest, &block)
787
+ TarOutput.open(dest, signer, &block)
679
788
  else
680
789
  raise "Unknown Package open mode"
681
790
  end
682
791
  end
683
792
 
684
- def self.open_from_io(io, mode = "r", &block)
793
+ def self.open_from_io(io, mode = "r", signer = nil, &block)
685
794
  raise "Block needed" unless block_given?
686
795
 
687
796
  case mode
688
797
  when "r"
689
- TarInput.open_from_io(io, &block)
798
+ security_policy = signer
799
+ TarInput.open_from_io(io, security_policy, &block)
690
800
  when "w"
691
- TarOutput.open_from_io(io, &block)
801
+ TarOutput.open_from_io(io, signer, &block)
692
802
  else
693
803
  raise "Unknown Package open mode"
694
804
  end
695
805
  end
696
806
 
697
- def self.pack(src, destname)
698
- TarOutput.open(destname) do |outp|
807
+ def self.pack(src, destname, signer = nil)
808
+ TarOutput.open(destname, signer) do |outp|
699
809
  dir_class.chdir(src) do
700
810
  outp.metadata = (file_class.read("RPA/metadata") rescue nil)
701
811
  find_class.find('.') do |entry|
@@ -63,7 +63,7 @@ module Gem
63
63
 
64
64
  # Normalize the URI by adding "http://" if it is missing.
65
65
  def normalize_uri(uri)
66
- (uri =~ /^(https?|ftp):/) ? uri : "http://#{uri}"
66
+ (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
67
67
  end
68
68
 
69
69
  # Connect to the source host/port, using a proxy if needed.
@@ -85,6 +85,8 @@ module Gem
85
85
  # Read the size of the (source based) URI using an HTTP HEAD
86
86
  # command.
87
87
  def read_size(uri)
88
+ return File.size(get_file_uri_path(uri)) if is_file_uri(uri)
89
+
88
90
  require 'net/http'
89
91
  require 'uri'
90
92
  u = URI.parse(uri)
@@ -97,22 +99,42 @@ module Gem
97
99
 
98
100
  # Read the data from the (source based) URI.
99
101
  def read_data(uri)
100
- require 'rubygems/open-uri'
101
102
  begin
102
- open(uri,
103
- "User-Agent" => "RubyGems/#{Gem::RubyGemsVersion}",
104
- :proxy => @http_proxy
105
- ) do |input|
106
- input.read
107
- end
103
+ open_uri_or_path(uri) do |input|
104
+ input.read
105
+ end
108
106
  rescue
109
- old_uri = uri
110
- uri = uri.downcase
111
- retry if old_uri != uri
112
- raise
107
+ old_uri = uri
108
+ uri = uri.downcase
109
+ retry if old_uri != uri
110
+ raise
113
111
  end
114
112
  end
115
-
113
+
114
+ # Read the data from the (source based) URI, but if it is a
115
+ # file:// URI, read from the filesystem instead.
116
+ def open_uri_or_path(uri, &block)
117
+ require 'rubygems/open-uri'
118
+ if is_file_uri(uri)
119
+ open(get_file_uri_path(uri), &block)
120
+ else
121
+ open(uri,
122
+ "User-Agent" => "RubyGems/#{Gem::RubyGemsVersion}",
123
+ :proxy => @http_proxy,
124
+ &block)
125
+ end
126
+ end
127
+
128
+ # Checks if the provided string is a file:// URI.
129
+ def is_file_uri(uri)
130
+ uri =~ %r{\Afile://}
131
+ end
132
+
133
+ # Given a file:// URI, returns its local path.
134
+ def get_file_uri_path(uri)
135
+ uri.sub(%r{\Afile://}, '')
136
+ end
137
+
116
138
  # Convert the yamlized string spec into a real spec (actually,
117
139
  # these are hashes of specs.).
118
140
  def convert_spec(yaml_spec)
@@ -425,36 +447,44 @@ module Gem
425
447
 
426
448
  # Find a gem to be installed by interacting with the user.
427
449
  def find_gem_to_install(gem_name, version_requirement, caches)
428
- max_version = Version.new("0.0.0")
429
450
  specs_n_sources = []
451
+
430
452
  caches.each do |source, cache|
431
453
  cache.each do |name, spec|
432
- if (/^#{gem_name}-/i === name &&
433
- version_requirement.satisfied_by?(spec.version))
454
+ if /^#{gem_name}$/i === spec.name &&
455
+ version_requirement.satisfied_by?(spec.version) then
434
456
  specs_n_sources << [spec, source]
435
457
  end
436
458
  end
437
459
  end
438
- if specs_n_sources.size == 0
439
- raise GemNotFoundException.new(
440
- "Could not find #{gem_name} (#{version_requirement}) in the repository")
441
- end
442
- specs_n_sources = specs_n_sources.sort_by { |x| x[0].version }.reverse
443
- if specs_n_sources.reject { |item|
444
- item[0].platform.nil? || item[0].platform==Platform::RUBY
445
- }.size == 0
446
- # only non-binary gems...return latest
447
- return specs_n_sources.first
460
+
461
+ if specs_n_sources.empty? then
462
+ raise GemNotFoundException.new("Could not find #{gem_name} (#{version_requirement}) in the repository")
448
463
  end
449
- list = specs_n_sources.collect {|item|
464
+
465
+ specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse
466
+
467
+ non_binary_gems = specs_n_sources.reject { |item|
468
+ item[0].platform.nil? || item[0].platform==Platform::RUBY
469
+ }
470
+
471
+ # only non-binary gems...return latest
472
+ return specs_n_sources.first if non_binary_gems.empty?
473
+
474
+ list = specs_n_sources.collect { |item|
450
475
  "#{item[0].name} #{item[0].version} (#{item[0].platform.to_s})"
451
476
  }
477
+
452
478
  list << "Cancel installation"
479
+
453
480
  string, index = choose_from_list(
454
481
  "Select which gem to install for your platform (#{RUBY_PLATFORM})",
455
482
  list)
456
- raise RemoteInstallationCancelled.new("Installation of #{gem_name} cancelled.") if
457
- index == (list.size - 1)
483
+
484
+ if index == (list.size - 1) then
485
+ raise RemoteInstallationCancelled, "Installation of #{gem_name} cancelled."
486
+ end
487
+
458
488
  specs_n_sources[index]
459
489
  end
460
490
 
@@ -2,5 +2,5 @@
2
2
  # This file is auto-generated by build scripts.
3
3
  # See: rake update_version
4
4
  module Gem
5
- RubyGemsVersion = '0.8.10'
5
+ RubyGemsVersion = '0.8.11'
6
6
  end
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems/gem_openssl'
4
+
5
+ module Gem
6
+ module SSL
7
+
8
+ # We make our own versions of the constants here. This allows us
9
+ # to reference the constants, even though some systems might not
10
+ # have SSL installed in the Ruby core package.
11
+ #
12
+ # These constants are only used during load time. At runtime, any
13
+ # method that makes a direct reference to SSL software must be
14
+ # protected with a Gem.ensure_ssl_available call.
15
+ #
16
+ if Gem.ssl_available?
17
+ PKEY_RSA = OpenSSL::PKey::RSA
18
+ DIGEST_SHA1 = OpenSSL::Digest::SHA1
19
+ else
20
+ PKEY_RSA = :rsa
21
+ DIGEST_SHA1 = :sha1
22
+ end
23
+ end
24
+ end
25
+
26
+ module OpenSSL
27
+ module X509
28
+ class Certificate
29
+ #
30
+ # Check the validity of this certificate.
31
+ #
32
+ def check_validity(issuer_cert = nil, time = Time.now)
33
+ ret = if @not_before && @not_before > time
34
+ [false, :expired, "not valid before '#@not_before'"]
35
+ elsif @not_after && @not_after < time
36
+ [false, :expired, "not valid after '#@not_after'"]
37
+ elsif issuer_cert && !verify(issuer_cert.public_key)
38
+ [false, :issuer, "#{issuer_cert.subject} is not issuer"]
39
+ else
40
+ [true, :ok, 'Valid certificate']
41
+ end
42
+
43
+ # return hash
44
+ { :is_valid => ret[0], :error => ret[1], :desc => ret[2] }
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ module Gem
51
+ #
52
+ # Security: a set of methods, classes, and security policies for
53
+ # checking the validity of signed gem files.
54
+ #
55
+ module Security
56
+ class Exception < Exception; end
57
+
58
+ #
59
+ # default options for most of the methods below
60
+ #
61
+ OPT = {
62
+ # private key options
63
+ :key_algo => Gem::SSL::PKEY_RSA,
64
+ :key_size => 2048,
65
+
66
+ # public cert options
67
+ :cert_age => 365 * 24 * 3600, # 1 year
68
+ :dgst_algo => Gem::SSL::DIGEST_SHA1,
69
+
70
+ # x509 certificate extensions
71
+ :cert_exts => {
72
+ 'basicConstraints' => 'CA:FALSE',
73
+ 'subjectKeyIdentifier' => 'hash',
74
+ 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature',
75
+ },
76
+
77
+ # save the key and cert to a file in build_self_signed_cert()?
78
+ :save_key => true,
79
+ :save_cert => true,
80
+
81
+ # if you define either of these, then they'll be used instead of
82
+ # the output_fmt macro below
83
+ :save_key_path => nil,
84
+ :save_cert_path => nil,
85
+
86
+ # output name format for self-signed certs
87
+ :output_fmt => 'gem-%s.pem',
88
+ :munge_re => Regexp.new(/[^a-z0-9_.-]+/),
89
+
90
+ # output directory for trusted certificate checksums
91
+ :trust_dir => File::join(Gem.user_home, '.gem', 'trust'),
92
+ }
93
+
94
+ #
95
+ # A Gem::Security::Policy object encapsulates the settings for
96
+ # verifying signed gem files. This is the base class. You can
97
+ # either declare an instance of this or use one of the preset
98
+ # security policies below.
99
+ #
100
+ class Policy
101
+ attr_accessor :verify_data, :verify_signer, :verify_chain,
102
+ :verify_root, :only_trusted, :only_signed
103
+
104
+ #
105
+ # Create a new Gem::Security::Policy object with the given mode
106
+ # and options.
107
+ #
108
+ def initialize(policy = {}, opt = {})
109
+ # set options
110
+ @opt = Gem::Security::OPT.merge(opt)
111
+
112
+ # build policy
113
+ policy.each_pair do |key, val|
114
+ case key
115
+ when :verify_data then @verify_data = val
116
+ when :verify_signer then @verify_signer = val
117
+ when :verify_chain then @verify_chain = val
118
+ when :verify_root then @verify_root = val
119
+ when :only_trusted then @only_trusted = val
120
+ when :only_signed then @only_signed = val
121
+ end
122
+ end
123
+ end
124
+
125
+ #
126
+ # Get the path to the file for this cert.
127
+ #
128
+ def self.trusted_cert_path(cert, opt = {})
129
+ opt = Gem::Security::OPT.merge(opt)
130
+
131
+ # get digest algorithm, calculate checksum of root.subject
132
+ algo = opt[:dgst_algo]
133
+ dgst = algo.hexdigest(cert.subject.to_s)
134
+
135
+ # build path to trusted cert file
136
+ name = "cert-#{dgst}.pem"
137
+
138
+ # join and return path components
139
+ File::join(opt[:trust_dir], name)
140
+ end
141
+
142
+ #
143
+ # Verify that the gem data with the given signature and signing
144
+ # chain matched this security policy at the specified time.
145
+ #
146
+ def verify_gem(signature, data, chain, time = Time.now)
147
+ Gem.ensure_ssl_available
148
+ cert_class = OpenSSL::X509::Certificate
149
+ exc = Gem::Security::Exception
150
+ chain ||= []
151
+
152
+ chain = chain.map{ |str| cert_class.new(str) }
153
+ signer, ch_len = chain[-1], chain.size
154
+
155
+ # make sure signature is valid
156
+ if @verify_data
157
+ # get digest algorithm (TODO: this should be configurable)
158
+ dgst = @opt[:dgst_algo]
159
+
160
+ # verify the data signature (this is the most important part,
161
+ # so don't screw it up :D)
162
+ v = signer.public_key.verify(dgst.new, signature, data)
163
+ raise exc, "Invalid Gem Signature" unless v
164
+
165
+ # make sure the signer is valid
166
+ if @verify_signer
167
+ # make sure the signing cert is valid right now
168
+ v = signer.check_validity(nil, time)
169
+ raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid]
170
+ end
171
+ end
172
+
173
+ # make sure the certificate chain is valid
174
+ if @verify_chain
175
+ # iterate down over the chain and verify each certificate
176
+ # against it's issuer
177
+ (ch_len - 1).downto(1) do |i|
178
+ issuer, cert = chain[i - 1, 2]
179
+ v = cert.check_validity(issuer, time)
180
+ raise exc, "%s: cert = '%s', error = '%s'" % [
181
+ 'Invalid Signing Chain', cert.subject, v[:desc]
182
+ ] unless v[:is_valid]
183
+ end
184
+
185
+ # verify root of chain
186
+ if @verify_root
187
+ # make sure root is self-signed
188
+ root = chain[0]
189
+ raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [
190
+ 'Invalid Signing Chain Root',
191
+ 'Subject does not match Issuer for Gem Signing Chain',
192
+ root.subject.to_s,
193
+ root.issuer.to_s,
194
+ ] unless root.issuer.to_s == root.subject.to_s
195
+
196
+ # make sure root is valid
197
+ v = root.check_validity(root, time)
198
+ raise exc, "%s: cert = '%s', error = '%s'" % [
199
+ 'Invalid Signing Chain Root', root.subject, v[:desc]
200
+ ] unless v[:is_valid]
201
+
202
+ # verify that the chain root is trusted
203
+ if @only_trusted
204
+ # get digest algorithm, calculate checksum of root.subject
205
+ algo = @opt[:dgst_algo]
206
+ path = Gem::Security::Policy.trusted_cert_path(root, @opt)
207
+
208
+ # check to make sure trusted path exists
209
+ raise exc, "%s: cert = '%s', error = '%s'" % [
210
+ 'Untrusted Signing Chain Root',
211
+ root.subject.to_s,
212
+ "path \"#{path}\" does not exist",
213
+ ] unless File.exists?(path)
214
+
215
+ # load calculate digest from saved cert file
216
+ save_cert = OpenSSL::X509::Certificate.new(File.read(path))
217
+ save_dgst = algo.digest(save_cert.public_key.to_s)
218
+
219
+ # create digest of public key
220
+ pkey_str = root.public_key.to_s
221
+ cert_dgst = algo.digest(pkey_str)
222
+
223
+ # now compare the two digests, raise exception
224
+ # if they don't match
225
+ raise exc, "%s: %s (saved = '%s', root = '%s')" % [
226
+ 'Invalid Signing Chain Root',
227
+ "Saved checksum doesn't match root checksum",
228
+ save_dgst, cert_dgst,
229
+ ] unless save_dgst == cert_dgst
230
+ end
231
+ end
232
+
233
+ # return the signing chain
234
+ chain.map { |cert| cert.subject }
235
+ end
236
+ end
237
+ end
238
+
239
+ #
240
+ # No security policy: all package signature checks are disabled.
241
+ #
242
+ NoSecurity = Policy.new({
243
+ :verify_data => false,
244
+ :verify_signer => false,
245
+ :verify_chain => false,
246
+ :verify_root => false,
247
+ :only_trusted => false,
248
+ :only_signed => false,
249
+ })
250
+
251
+ #
252
+ # AlmostNo security policy: only verify that the signing certificate
253
+ # is the one that actually signed the data. Make no attempt to
254
+ # verify the signing certificate chain.
255
+ #
256
+ # This policy is basically useless. better than nothing, but can still be easily
257
+ # spoofed, and is not recommended.
258
+ #
259
+ AlmostNoSecurity = Policy.new({
260
+ :verify_data => true,
261
+ :verify_signer => false,
262
+ :verify_chain => false,
263
+ :verify_root => false,
264
+ :only_trusted => false,
265
+ :only_signed => false,
266
+ })
267
+
268
+ #
269
+ # Low security policy: only verify that the signing certificate is
270
+ # actually the gem signer, and that the signing certificate is
271
+ # valid.
272
+ #
273
+ # This policy is better than nothing, but can still be easily
274
+ # spoofed, and is not recommended.
275
+ #
276
+ LowSecurity = Policy.new({
277
+ :verify_data => true,
278
+ :verify_signer => true,
279
+ :verify_chain => false,
280
+ :verify_root => false,
281
+ :only_trusted => false,
282
+ :only_signed => false,
283
+ })
284
+
285
+ #
286
+ # Medium security policy: verify the signing certificate, verify the
287
+ # signing certificate chain all the way to the root certificate, and
288
+ # only trust root certificates that we have explicity allowed trust
289
+ # for.
290
+ #
291
+ # This security policy is reasonable, but it allows unsigned
292
+ # packages, so a malicious person could simply delete the package
293
+ # signature and pass the gem off as unsigned.
294
+ #
295
+ MediumSecurity = Policy.new({
296
+ :verify_data => true,
297
+ :verify_signer => true,
298
+ :verify_chain => true,
299
+ :verify_root => true,
300
+ :only_trusted => true,
301
+ :only_signed => false,
302
+ })
303
+
304
+ #
305
+ # High security policy: only allow signed gems to be installed,
306
+ # verify the signing certificate, verify the signing certificate
307
+ # chain all the way to the root certificate, and only trust root
308
+ # certificates that we have explicity allowed trust for.
309
+ #
310
+ # This security policy is significantly more difficult to bypass,
311
+ # and offers a reasonable guarantee that the contents of the gem
312
+ # have not been altered.
313
+ #
314
+ HighSecurity = Policy.new({
315
+ :verify_data => true,
316
+ :verify_signer => true,
317
+ :verify_chain => true,
318
+ :verify_root => true,
319
+ :only_trusted => true,
320
+ :only_signed => true,
321
+ })
322
+
323
+ #
324
+ # Sign the cert cert with @signing_key and @signing_cert, using the
325
+ # digest algorithm opt[:dgst_algo]. Returns the newly signed
326
+ # certificate.
327
+ #
328
+ def self.sign_cert(cert, signing_key, signing_cert, opt = {})
329
+ opt = OPT.merge(opt)
330
+
331
+ # set up issuer information
332
+ cert.issuer = signing_cert.subject
333
+ cert.sign(signing_key, opt[:dgst_algo].new)
334
+
335
+ cert
336
+ end
337
+
338
+ #
339
+ # Build a certificate from the given DN and private key.
340
+ #
341
+ def self.build_cert(name, key, opt = {})
342
+ Gem.ensure_ssl_available
343
+ opt = OPT.merge(opt)
344
+
345
+ # create new cert
346
+ ret = OpenSSL::X509::Certificate.new
347
+
348
+ # populate cert attributes
349
+ ret.version = 2
350
+ ret.serial = 0
351
+ ret.public_key = key.public_key
352
+ ret.not_before = Time.now
353
+ ret.not_after = Time.now + opt[:cert_age]
354
+ ret.subject = name
355
+
356
+ # add certificate extensions
357
+ ef = OpenSSL::X509::ExtensionFactory.new(nil, ret)
358
+ ret.extensions = opt[:cert_exts].map { |k, v| ef.create_extension(k, v) }
359
+
360
+ # sign cert
361
+ i_key, i_cert = opt[:issuer_key] || key, opt[:issuer_cert] || ret
362
+ ret = sign_cert(ret, i_key, i_cert, opt)
363
+
364
+ # return cert
365
+ ret
366
+ end
367
+
368
+ #
369
+ # Build a self-signed certificate for the given email address.
370
+ #
371
+ def self.build_self_signed_cert(email_addr, opt = {})
372
+ Gem.ensure_ssl_available
373
+ opt = OPT.merge(opt)
374
+ path = { :key => nil, :cert => nil }
375
+
376
+ # split email address up
377
+ cn, dcs = email_addr.split('@')
378
+ dcs = dcs.split('.')
379
+
380
+ # munge email CN and DCs
381
+ cn = cn.gsub(opt[:munge_re], '_')
382
+ dcs = dcs.map { |dc| dc.gsub(opt[:munge_re], '_') }
383
+
384
+ # create DN
385
+ name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/')
386
+ name = OpenSSL::X509::Name::parse(name)
387
+
388
+ # build private key
389
+ key = opt[:key_algo].new(opt[:key_size])
390
+
391
+ # create the trust directory if it doesn't exist
392
+ FileUtils::mkdir_p(opt[:trust_dir]) unless File.exists?(opt[:trust_dir])
393
+
394
+ # if we're saving the key, then write it out
395
+ if opt[:save_key]
396
+ path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key')
397
+ File.open(path[:key], 'wb') { |file| file.write(key.to_pem) }
398
+ end
399
+
400
+ # build self-signed public cert from key
401
+ cert = build_cert(name, key, opt)
402
+
403
+ # if we're saving the cert, then write it out
404
+ if opt[:save_cert]
405
+ path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert')
406
+ File.open(path[:cert], 'wb') { |file| file.write(cert.to_pem) }
407
+ end
408
+
409
+ # return key, cert, and paths (if applicable)
410
+ { :key => key, :cert => cert,
411
+ :key_path => path[:key], :cert_path => path[:cert] }
412
+ end
413
+
414
+ #
415
+ # Add certificate to trusted cert list.
416
+ #
417
+ # Note: At the moment these are stored in OPT[:trust_dir], although
418
+ # that directory may change in the future.
419
+ #
420
+ def self.add_trusted_cert(cert, opt = {})
421
+ opt = OPT.merge(opt)
422
+
423
+ # get destination path
424
+ path = Gem::Security::Policy.trusted_cert_path(cert, opt)
425
+
426
+ # write cert to output file
427
+ File.open(path, 'wb') { |file| file.write(cert.to_pem) }
428
+
429
+ # return nil
430
+ nil
431
+ end
432
+
433
+ #
434
+ # Basic OpenSSL-based package signing class.
435
+ #
436
+ class Signer
437
+ attr_accessor :key, :cert_chain
438
+
439
+ def initialize(key, cert_chain)
440
+ Gem.ensure_ssl_available
441
+ @algo = Gem::Security::OPT[:dgst_algo]
442
+ @key, @cert_chain = key, cert_chain
443
+
444
+ # check key, if it's a file, and if it's key, leave it alone
445
+ if @key && !@key.kind_of?(OpenSSL::PKey::PKey)
446
+ @key = OpenSSL::PKey::RSA.new(File.read(@key))
447
+ end
448
+
449
+ # check cert chain, if it's a file, load it, if it's cert data, convert
450
+ # it into a cert object, and if it's a cert object, leave it alone
451
+ if @cert_chain
452
+ @cert_chain = @cert_chain.map do |cert|
453
+ # check cert, if it's a file, load it, if it's cert data,
454
+ # convert it into a cert object, and if it's a cert object,
455
+ # leave it alone
456
+ if cert && !cert.kind_of?(OpenSSL::X509::Certificate)
457
+ cert = File.read(cert) if File::exists?(cert)
458
+ cert = OpenSSL::X509::Certificate.new(cert)
459
+ end
460
+ cert
461
+ end
462
+ end
463
+ end
464
+
465
+ #
466
+ # Sign data with given digest algorithm
467
+ #
468
+ def sign(data)
469
+ @key.sign(@algo.new, data)
470
+ end
471
+
472
+ # moved to security policy (see above)
473
+ # def verify(sig, data)
474
+ # @cert.public_key.verify(@algo.new, sig, data)
475
+ # end
476
+ end
477
+ end
478
+ end