summaryrefslogtreecommitdiff
path: root/jni/ruby/lib/rubygems/security
diff options
context:
space:
mode:
authorJari Vetoniemi <jari.vetoniemi@indooratlas.com>2020-03-16 18:49:26 +0900
committerJari Vetoniemi <jari.vetoniemi@indooratlas.com>2020-03-30 00:39:06 +0900
commitfcbf63e62c627deae76c1b8cb8c0876c536ed811 (patch)
tree64cb17de3f41a2b6fef2368028fbd00349946994 /jni/ruby/lib/rubygems/security
Fresh start
Diffstat (limited to 'jni/ruby/lib/rubygems/security')
-rw-r--r--jni/ruby/lib/rubygems/security/policies.rb115
-rw-r--r--jni/ruby/lib/rubygems/security/policy.rb295
-rw-r--r--jni/ruby/lib/rubygems/security/signer.rb154
-rw-r--r--jni/ruby/lib/rubygems/security/trust_dir.rb118
4 files changed, 682 insertions, 0 deletions
diff --git a/jni/ruby/lib/rubygems/security/policies.rb b/jni/ruby/lib/rubygems/security/policies.rb
new file mode 100644
index 0000000..a976eca
--- /dev/null
+++ b/jni/ruby/lib/rubygems/security/policies.rb
@@ -0,0 +1,115 @@
+module Gem::Security
+
+ ##
+ # No security policy: all package signature checks are disabled.
+
+ NoSecurity = Policy.new(
+ 'No Security',
+ :verify_data => false,
+ :verify_signer => false,
+ :verify_chain => false,
+ :verify_root => false,
+ :only_trusted => false,
+ :only_signed => false
+ )
+
+ ##
+ # AlmostNo security policy: only verify that the signing certificate is the
+ # one that actually signed the data. Make no attempt to verify the signing
+ # certificate chain.
+ #
+ # This policy is basically useless. better than nothing, but can still be
+ # easily spoofed, and is not recommended.
+
+ AlmostNoSecurity = Policy.new(
+ 'Almost No Security',
+ :verify_data => true,
+ :verify_signer => false,
+ :verify_chain => false,
+ :verify_root => false,
+ :only_trusted => false,
+ :only_signed => false
+ )
+
+ ##
+ # Low security policy: only verify that the signing certificate is actually
+ # the gem signer, and that the signing certificate is valid.
+ #
+ # This policy is better than nothing, but can still be easily spoofed, and
+ # is not recommended.
+
+ LowSecurity = Policy.new(
+ 'Low Security',
+ :verify_data => true,
+ :verify_signer => true,
+ :verify_chain => false,
+ :verify_root => false,
+ :only_trusted => false,
+ :only_signed => false
+ )
+
+ ##
+ # Medium security policy: verify the signing certificate, verify the signing
+ # certificate chain all the way to the root certificate, and only trust root
+ # certificates that we have explicitly allowed trust for.
+ #
+ # This security policy is reasonable, but it allows unsigned packages, so a
+ # malicious person could simply delete the package signature and pass the
+ # gem off as unsigned.
+
+ MediumSecurity = Policy.new(
+ 'Medium Security',
+ :verify_data => true,
+ :verify_signer => true,
+ :verify_chain => true,
+ :verify_root => true,
+ :only_trusted => true,
+ :only_signed => false
+ )
+
+ ##
+ # High security policy: only allow signed gems to be installed, verify the
+ # signing certificate, verify the signing certificate chain all the way to
+ # the root certificate, and only trust root certificates that we have
+ # explicitly allowed trust for.
+ #
+ # This security policy is significantly more difficult to bypass, and offers
+ # a reasonable guarantee that the contents of the gem have not been altered.
+
+ HighSecurity = Policy.new(
+ 'High Security',
+ :verify_data => true,
+ :verify_signer => true,
+ :verify_chain => true,
+ :verify_root => true,
+ :only_trusted => true,
+ :only_signed => true
+ )
+
+ ##
+ # Policy used to verify a certificate and key when signing a gem
+
+ SigningPolicy = Policy.new(
+ 'Signing Policy',
+ :verify_data => false,
+ :verify_signer => true,
+ :verify_chain => true,
+ :verify_root => true,
+ :only_trusted => false,
+ :only_signed => false
+ )
+
+ ##
+ # Hash of configured security policies
+
+ Policies = {
+ 'NoSecurity' => NoSecurity,
+ 'AlmostNoSecurity' => AlmostNoSecurity,
+ 'LowSecurity' => LowSecurity,
+ 'MediumSecurity' => MediumSecurity,
+ 'HighSecurity' => HighSecurity,
+ # SigningPolicy is not intended for use by `gem -P` so do not list it
+ }
+
+end
+
diff --git a/jni/ruby/lib/rubygems/security/policy.rb b/jni/ruby/lib/rubygems/security/policy.rb
new file mode 100644
index 0000000..b9bcb17
--- /dev/null
+++ b/jni/ruby/lib/rubygems/security/policy.rb
@@ -0,0 +1,295 @@
+require 'rubygems/user_interaction'
+
+##
+# A Gem::Security::Policy object encapsulates the settings for verifying
+# signed gem files. This is the base class. You can either declare an
+# instance of this or use one of the preset security policies in
+# Gem::Security::Policies.
+
+class Gem::Security::Policy
+
+ include Gem::UserInteraction
+
+ attr_reader :name
+
+ attr_accessor :only_signed
+ attr_accessor :only_trusted
+ attr_accessor :verify_chain
+ attr_accessor :verify_data
+ attr_accessor :verify_root
+ attr_accessor :verify_signer
+
+ ##
+ # Create a new Gem::Security::Policy object with the given mode and
+ # options.
+
+ def initialize name, policy = {}, opt = {}
+ require 'openssl'
+
+ @name = name
+
+ @opt = opt
+
+ # Default to security
+ @only_signed = true
+ @only_trusted = true
+ @verify_chain = true
+ @verify_data = true
+ @verify_root = true
+ @verify_signer = true
+
+ policy.each_pair do |key, val|
+ case key
+ when :verify_data then @verify_data = val
+ when :verify_signer then @verify_signer = val
+ when :verify_chain then @verify_chain = val
+ when :verify_root then @verify_root = val
+ when :only_trusted then @only_trusted = val
+ when :only_signed then @only_signed = val
+ end
+ end
+ end
+
+ ##
+ # Verifies each certificate in +chain+ has signed the following certificate
+ # and is valid for the given +time+.
+
+ def check_chain chain, time
+ raise Gem::Security::Exception, 'missing signing chain' unless chain
+ raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
+
+ begin
+ chain.each_cons 2 do |issuer, cert|
+ check_cert cert, issuer, time
+ end
+
+ true
+ rescue Gem::Security::Exception => e
+ raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
+ end
+ end
+
+ ##
+ # Verifies that +data+ matches the +signature+ created by +public_key+ and
+ # the +digest+ algorithm.
+
+ def check_data public_key, digest, signature, data
+ raise Gem::Security::Exception, "invalid signature" unless
+ public_key.verify digest.new, signature, data.digest
+
+ true
+ end
+
+ ##
+ # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
+ # If the +issuer+ is +nil+ no verification is performed.
+
+ def check_cert signer, issuer, time
+ raise Gem::Security::Exception, 'missing signing certificate' unless
+ signer
+
+ message = "certificate #{signer.subject}"
+
+ if not_before = signer.not_before and not_before > time then
+ raise Gem::Security::Exception,
+ "#{message} not valid before #{not_before}"
+ end
+
+ if not_after = signer.not_after and not_after < time then
+ raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
+ end
+
+ if issuer and not signer.verify issuer.public_key then
+ raise Gem::Security::Exception,
+ "#{message} was not issued by #{issuer.subject}"
+ end
+
+ true
+ end
+
+ ##
+ # Ensures the public key of +key+ matches the public key in +signer+
+
+ def check_key signer, key
+ unless signer and key then
+ return true unless @only_signed
+
+ raise Gem::Security::Exception, 'missing key or signature'
+ end
+
+ raise Gem::Security::Exception,
+ "certificate #{signer.subject} does not match the signing key" unless
+ signer.public_key.to_pem == key.public_key.to_pem
+
+ true
+ end
+
+ ##
+ # Ensures the root certificate in +chain+ is self-signed and valid for
+ # +time+.
+
+ def check_root chain, time
+ raise Gem::Security::Exception, 'missing signing chain' unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, 'missing root certificate' unless root
+
+ raise Gem::Security::Exception,
+ "root certificate #{root.subject} is not self-signed " +
+ "(issuer #{root.issuer})" if
+ root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
+
+ check_cert root, root, time
+ end
+
+ ##
+ # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
+ # the digests of the two certificates match according to +digester+
+
+ def check_trust chain, digester, trust_dir
+ raise Gem::Security::Exception, 'missing signing chain' unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, 'missing root certificate' unless root
+
+ path = Gem::Security.trust_dir.cert_path root
+
+ unless File.exist? path then
+ message = "root cert #{root.subject} is not trusted"
+
+ message << " (root of signing cert #{chain.last.subject})" if
+ chain.length > 1
+
+ raise Gem::Security::Exception, message
+ end
+
+ save_cert = OpenSSL::X509::Certificate.new File.read path
+ save_dgst = digester.digest save_cert.public_key.to_s
+
+ pkey_str = root.public_key.to_s
+ cert_dgst = digester.digest pkey_str
+
+ raise Gem::Security::Exception,
+ "trusted root certificate #{root.subject} checksum " +
+ "does not match signing root certificate checksum" unless
+ save_dgst == cert_dgst
+
+ true
+ end
+
+ ##
+ # Extracts the email or subject from +certificate+
+
+ def subject certificate # :nodoc:
+ certificate.extensions.each do |extension|
+ next unless extension.oid == 'subjectAltName'
+
+ return extension.value
+ end
+
+ certificate.subject.to_s
+ end
+
+ def inspect # :nodoc:
+ ("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
+ "signed-only: %p trusted-only: %p]") % [
+ @name, @verify_chain, @verify_data, @verify_root, @verify_signer,
+ @only_signed, @only_trusted,
+ ]
+ end
+
+ ##
+ # For +full_name+, verifies the certificate +chain+ is valid, the +digests+
+ # match the signatures +signatures+ created by the signer depending on the
+ # +policy+ settings.
+ #
+ # If +key+ is given it is used to validate the signing certificate.
+
+ def verify chain, key = nil, digests = {}, signatures = {},
+ full_name = '(unknown)'
+ if signatures.empty? then
+ if @only_signed then
+ raise Gem::Security::Exception,
+ "unsigned gems are not allowed by the #{name} policy"
+ elsif digests.empty? then
+ # lack of signatures is irrelevant if there is nothing to check
+ # against
+ else
+ alert_warning "#{full_name} is not signed"
+ return
+ end
+ end
+
+ opt = @opt
+ digester = Gem::Security::DIGEST_ALGORITHM
+ trust_dir = opt[:trust_dir]
+ time = Time.now
+
+ _, signer_digests = digests.find do |algorithm, file_digests|
+ file_digests.values.first.name == Gem::Security::DIGEST_NAME
+ end
+
+ if @verify_data then
+ raise Gem::Security::Exception, 'no digests provided (probable bug)' if
+ signer_digests.nil? or signer_digests.empty?
+ else
+ signer_digests = {}
+ end
+
+ signer = chain.last
+
+ check_key signer, key if key
+
+ check_cert signer, nil, time if @verify_signer
+
+ check_chain chain, time if @verify_chain
+
+ check_root chain, time if @verify_root
+
+ if @only_trusted then
+ check_trust chain, digester, trust_dir
+ elsif signatures.empty? and digests.empty? then
+ # trust is irrelevant if there's no signatures to verify
+ else
+ alert_warning "#{subject signer} is not trusted for #{full_name}"
+ end
+
+ signatures.each do |file, _|
+ digest = signer_digests[file]
+
+ raise Gem::Security::Exception, "missing digest for #{file}" unless
+ digest
+ end
+
+ signer_digests.each do |file, digest|
+ signature = signatures[file]
+
+ raise Gem::Security::Exception, "missing signature for #{file}" unless
+ signature
+
+ check_data signer.public_key, digester, signature, digest if @verify_data
+ end
+
+ true
+ end
+
+ ##
+ # Extracts the certificate chain from the +spec+ and calls #verify to ensure
+ # the signatures and certificate chain is valid according to the policy..
+
+ def verify_signatures spec, digests, signatures
+ chain = spec.cert_chain.map do |cert_pem|
+ OpenSSL::X509::Certificate.new cert_pem
+ end
+
+ verify chain, nil, digests, signatures, spec.full_name
+
+ true
+ end
+
+ alias to_s name # :nodoc:
+
+end
+
diff --git a/jni/ruby/lib/rubygems/security/signer.rb b/jni/ruby/lib/rubygems/security/signer.rb
new file mode 100644
index 0000000..bb1eae7
--- /dev/null
+++ b/jni/ruby/lib/rubygems/security/signer.rb
@@ -0,0 +1,154 @@
+##
+# Basic OpenSSL-based package signing class.
+
+class Gem::Security::Signer
+
+ ##
+ # The chain of certificates for signing including the signing certificate
+
+ attr_accessor :cert_chain
+
+ ##
+ # The private key for the signing certificate
+
+ attr_accessor :key
+
+ ##
+ # The digest algorithm used to create the signature
+
+ attr_reader :digest_algorithm
+
+ ##
+ # The name of the digest algorithm, used to pull digests out of the hash by
+ # name.
+
+ attr_reader :digest_name # :nodoc:
+
+ ##
+ # Creates a new signer with an RSA +key+ or path to a key, and a certificate
+ # +chain+ containing X509 certificates, encoding certificates or paths to
+ # certificates.
+
+ def initialize key, cert_chain, passphrase = nil
+ @cert_chain = cert_chain
+ @key = key
+
+ unless @key then
+ default_key = File.join Gem.default_key_path
+ @key = default_key if File.exist? default_key
+ end
+
+ unless @cert_chain then
+ default_cert = File.join Gem.default_cert_path
+ @cert_chain = [default_cert] if File.exist? default_cert
+ end
+
+ @digest_algorithm = Gem::Security::DIGEST_ALGORITHM
+ @digest_name = Gem::Security::DIGEST_NAME
+
+ @key = OpenSSL::PKey::RSA.new File.read(@key), passphrase if
+ @key and not OpenSSL::PKey::RSA === @key
+
+ if @cert_chain then
+ @cert_chain = @cert_chain.compact.map do |cert|
+ next cert if OpenSSL::X509::Certificate === cert
+
+ cert = File.read cert if File.exist? cert
+
+ OpenSSL::X509::Certificate.new cert
+ end
+
+ load_cert_chain
+ end
+ end
+
+ ##
+ # Extracts the full name of +cert+. If the certificate has a subjectAltName
+ # this value is preferred, otherwise the subject is used.
+
+ def extract_name cert # :nodoc:
+ subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid }
+
+ if subject_alt_name then
+ /\Aemail:/ =~ subject_alt_name.value
+
+ $' || subject_alt_name.value
+ else
+ cert.subject
+ end
+ end
+
+ ##
+ # Loads any missing issuers in the cert chain from the trusted certificates.
+ #
+ # If the issuer does not exist it is ignored as it will be checked later.
+
+ def load_cert_chain # :nodoc:
+ return if @cert_chain.empty?
+
+ while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do
+ issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first
+
+ break unless issuer # cert chain is verified later
+
+ @cert_chain.unshift issuer
+ end
+ end
+
+ ##
+ # Sign data with given digest algorithm
+
+ def sign data
+ return unless @key
+
+ if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now then
+ re_sign_key
+ end
+
+ full_name = extract_name @cert_chain.last
+
+ Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
+
+ @key.sign @digest_algorithm.new, data
+ end
+
+ ##
+ # Attempts to re-sign the private key if the signing certificate is expired.
+ #
+ # The key will be re-signed if:
+ # * The expired certificate is self-signed
+ # * The expired certificate is saved at ~/.gem/gem-public_cert.pem
+ # * There is no file matching the expiry date at
+ # ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S
+ #
+ # If the signing certificate can be re-signed the expired certificate will
+ # be saved as ~/.gem/gem-pubilc_cert.pem.expired.%Y%m%d%H%M%S where the
+ # expiry time (not after) is used for the timestamp.
+
+ def re_sign_key # :nodoc:
+ old_cert = @cert_chain.last
+
+ disk_cert_path = File.join Gem.default_cert_path
+ disk_cert = File.read disk_cert_path rescue nil
+ disk_key =
+ File.read File.join(Gem.default_key_path) rescue nil
+
+ if disk_key == @key.to_pem and disk_cert == old_cert.to_pem then
+ expiry = old_cert.not_after.strftime '%Y%m%d%H%M%S'
+ old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
+ old_cert_path = File.join Gem.user_home, ".gem", old_cert_file
+
+ unless File.exist? old_cert_path then
+ Gem::Security.write old_cert, old_cert_path
+
+ cert = Gem::Security.re_sign old_cert, @key
+
+ Gem::Security.write cert, disk_cert_path
+
+ @cert_chain = [cert]
+ end
+ end
+ end
+
+end
+
diff --git a/jni/ruby/lib/rubygems/security/trust_dir.rb b/jni/ruby/lib/rubygems/security/trust_dir.rb
new file mode 100644
index 0000000..76ef89a
--- /dev/null
+++ b/jni/ruby/lib/rubygems/security/trust_dir.rb
@@ -0,0 +1,118 @@
+##
+# The TrustDir manages the trusted certificates for gem signature
+# verification.
+
+class Gem::Security::TrustDir
+
+ ##
+ # Default permissions for the trust directory and its contents
+
+ DEFAULT_PERMISSIONS = {
+ :trust_dir => 0700,
+ :trusted_cert => 0600,
+ }
+
+ ##
+ # The directory where trusted certificates will be stored.
+
+ attr_reader :dir
+
+ ##
+ # Creates a new TrustDir using +dir+ where the directory and file
+ # permissions will be checked according to +permissions+
+
+ def initialize dir, permissions = DEFAULT_PERMISSIONS
+ @dir = dir
+ @permissions = permissions
+
+ @digester = Gem::Security::DIGEST_ALGORITHM
+ end
+
+ ##
+ # Returns the path to the trusted +certificate+
+
+ def cert_path certificate
+ name_path certificate.subject
+ end
+
+ ##
+ # Enumerates trusted certificates.
+
+ def each_certificate
+ return enum_for __method__ unless block_given?
+
+ glob = File.join @dir, '*.pem'
+
+ Dir[glob].each do |certificate_file|
+ begin
+ certificate = load_certificate certificate_file
+
+ yield certificate, certificate_file
+ rescue OpenSSL::X509::CertificateError
+ next # HACK warn
+ end
+ end
+ end
+
+ ##
+ # Returns the issuer certificate of the given +certificate+ if it exists in
+ # the trust directory.
+
+ def issuer_of certificate
+ path = name_path certificate.issuer
+
+ return unless File.exist? path
+
+ load_certificate path
+ end
+
+ ##
+ # Returns the path to the trusted certificate with the given ASN.1 +name+
+
+ def name_path name
+ digest = @digester.hexdigest name.to_s
+
+ File.join @dir, "cert-#{digest}.pem"
+ end
+
+ ##
+ # Loads the given +certificate_file+
+
+ def load_certificate certificate_file
+ pem = File.read certificate_file
+
+ OpenSSL::X509::Certificate.new pem
+ end
+
+ ##
+ # Add a certificate to trusted certificate list.
+
+ def trust_cert certificate
+ verify
+
+ destination = cert_path certificate
+
+ open destination, 'wb', @permissions[:trusted_cert] do |io|
+ io.write certificate.to_pem
+ end
+ end
+
+ ##
+ # Make sure the trust directory exists. If it does exist, make sure it's
+ # actually a directory. If not, then create it with the appropriate
+ # permissions.
+
+ def verify
+ if File.exist? @dir then
+ raise Gem::Security::Exception,
+ "trust directory #{@dir} is not a directory" unless
+ File.directory? @dir
+
+ FileUtils.chmod 0700, @dir
+ else
+ FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir]
+ end
+ end
+
+end
+