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.
- 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
|