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.
- data/ChangeLog +66 -0
- data/README +17 -3
- data/Rakefile +29 -11
- data/bin/gem_mirror +67 -0
- data/examples/application/an-app.gemspec +2 -0
- data/lib/rubygems.rb +18 -3
- data/lib/rubygems/builder.rb +14 -1
- data/lib/rubygems/cmd_manager.rb +2 -0
- data/lib/rubygems/command.rb +26 -2
- data/lib/rubygems/custom_require.rb +30 -21
- data/lib/rubygems/format.rb +4 -4
- data/lib/rubygems/gem_commands.rb +161 -9
- data/lib/rubygems/gem_openssl.rb +34 -0
- data/lib/rubygems/gem_runner.rb +5 -1
- data/lib/rubygems/installer.rb +117 -38
- data/lib/rubygems/package.rb +135 -25
- data/lib/rubygems/remote_installer.rb +59 -29
- data/lib/rubygems/rubygems_version.rb +1 -1
- data/lib/rubygems/security.rb +478 -0
- data/lib/rubygems/specification.rb +48 -28
- data/post-install.rb +2 -1
- data/scripts/gemdoc.rb +2 -2
- data/scripts/specdoc.rb +25 -24
- data/scripts/upload_gemdoc.rb +134 -0
- data/setup.rb +1 -1
- data/test/data/a-0.0.1.gem +0 -0
- data/test/data/a-0.0.2.gem +0 -0
- data/test/data/b-0.0.2.gem +0 -0
- data/test/data/c-1.2.gem +0 -0
- data/test/data/gemhome/cache/a-0.0.1.gem +0 -0
- data/test/data/gemhome/cache/a-0.0.2.gem +0 -0
- data/test/data/gemhome/cache/b-0.0.2.gem +0 -0
- data/test/data/gemhome/cache/c-1.2.gem +0 -0
- data/test/data/gemhome/specifications/a-0.0.1.gemspec +1 -1
- data/test/data/gemhome/specifications/a-0.0.2.gemspec +1 -1
- data/test/data/gemhome/specifications/b-0.0.2.gemspec +1 -1
- data/test/data/gemhome/specifications/c-1.2.gemspec +1 -1
- data/test/data/one/one-0.0.1.gem +0 -0
- data/test/fake_certlib/openssl.rb +1 -0
- data/test/functional.rb +49 -14
- data/test/gemutilities.rb +69 -5
- data/test/test_cached_fetcher.rb +5 -7
- data/test/test_file_list.rb +96 -0
- data/test/test_gempaths.rb +36 -34
- data/test/test_installer.rb +214 -0
- data/test/test_local_cache.rb +45 -102
- data/test/test_parse_commands.rb +3 -1
- data/test/test_remote_installer.rb +24 -5
- data/test/test_specific_extras.rb +40 -0
- data/test/test_specification.rb +106 -16
- metadata +14 -3
data/lib/rubygems/package.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
547
|
-
|
548
|
-
|
611
|
+
zis = Zlib::GzipReader.new entry
|
612
|
+
dis = zis.read
|
613
|
+
is = StringIO.new(dis)
|
549
614
|
else
|
550
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
433
|
-
|
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
|
-
|
439
|
-
|
440
|
-
|
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
|
-
|
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
|
-
|
457
|
-
|
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
|
|
@@ -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
|