rubygems-update 0.8.10 → 0.8.11

Sign up to get free protection for your applications and to get access to all the features.

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