diff options
author | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2012-11-29 06:52:18 +0000 |
---|---|---|
committer | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2012-11-29 06:52:18 +0000 |
commit | 9694bb8cac12969300692dac5a1cf7aa4e3a46cd (patch) | |
tree | c3cb423d701f7049ba9382de052e2a937cd1302d /lib/rubygems/security.rb | |
parent | 3f606b7063fc7a8b191556365ad343a314719a8d (diff) | |
download | ruby-9694bb8cac12969300692dac5a1cf7aa4e3a46cd.tar.gz |
* lib/rubygems*: Updated to RubyGems 2.0
* test/rubygems*: ditto.
* common.mk (prelude): Updated for RubyGems 2.0 source rearrangement.
* tool/change_maker.rb: Allow invalid UTF-8 characters in source
files.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@37976 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rubygems/security.rb')
-rw-r--r-- | lib/rubygems/security.rb | 850 |
1 files changed, 295 insertions, 555 deletions
diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index f51da65b4b..bec30e9238 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -5,80 +5,89 @@ #++ require 'rubygems/exceptions' -require 'rubygems/gem_openssl' +require 'openssl' require 'fileutils' +## +# = Signing gems # -# = Signed Gems README -# -# == Table of Contents -# * Overview -# * Walkthrough -# * Command-Line Options -# * OpenSSL Reference -# * Bugs/TODO -# * About the Author -# -# == Overview -# -# Gem::Security implements cryptographic signatures in RubyGems. The section +# The Gem::Security implements cryptographic signatures for gems. The section # below is a step-by-step guide to using signed gems and generating your own. # # == Walkthrough # +# === Building your certificate +# # In order to start signing your gems, you'll need to build a private key and # a self-signed certificate. Here's how: # -# # build a private key and certificate for gemmaster@example.com -# $ gem cert --build gemmaster@example.com +# # build a private key and certificate for yourself: +# $ gem cert --build you@example.com # -# This could take anywhere from 5 seconds to 10 minutes, depending on the -# speed of your computer (public key algorithms aren't exactly the speediest -# crypto algorithms in the world). When it's finished, you'll see the files -# "gem-private_key.pem" and "gem-public_cert.pem" in the current directory. +# This could take anywhere from a few seconds to a minute or two, depending on +# the speed of your computer (public key algorithms aren't exactly the +# speediest crypto algorithms in the world). When it's finished, you'll see +# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current +# directory. # -# First things first: take the "gem-private_key.pem" file and move it -# somewhere private, preferably a directory only you have access to, a floppy -# (yuck!), a CD-ROM, or something comparably secure. Keep your private key -# hidden; if it's compromised, someone can sign packages as you (note: PKI has -# ways of mitigating the risk of stolen keys; more on that later). +# First things first: Move both files to ~/.gem if you don't already have a +# key and certificate in that directory. Ensure the file permissions make the +# key unreadable by others (by default the file is saved securely). # -# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but -# you can use whatever gem you'd like. Open up your existing gemspec file and -# add the following lines: +# Keep your private key hidden; if it's compromised, someone can sign packages +# as you (note: PKI has ways of mitigating the risk of stolen keys; more on +# that later). # -# # signing key and certificate chain -# s.signing_key = '/mnt/floppy/gem-private_key.pem' -# s.cert_chain = ['gem-public_cert.pem'] +# === Signing Gems # -# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private -# key). +# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will +# automatically find your key and certificate in your home directory and use +# them to sign newly packaged gems. # -# After that, go ahead and build your gem as usual. Congratulations, you've -# just built your first signed gem! If you peek inside your gem file, you'll -# see a couple of new files have been added: +# If your certificate is not self-signed (signed by a third party) RubyGems +# will attempt to load the certificate chain from the trusted certificates. +# Use <code>gem cert --add signing_cert.pem</code> to add your signers as +# trusted certificates. See below for further information on certificate +# chains. # -# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem -# data.tar.gz -# data.tar.gz.sig +# If you build your gem it will automatically be signed. If you peek inside +# your gem file, you'll see a couple of new files have been added: +# +# $ tar tf your-gem-1.0.gem # metadata.gz -# metadata.gz.sig +# metadata.gz.sum +# metadata.gz.sig # metadata signature +# data.tar.gz +# data.tar.gz.sum +# data.tar.gz.sig # data signature +# +# === Manually signing gems +# +# If you wish to store your key in a separate secure location you'll need to +# set your gems up for signing by hand. To do this, set the +# <code>signing_key</code> and <code>cert_chain</code> in the gemspec before +# packaging your gem: +# +# s.signing_key = '/secure/path/to/gem-private_key.pem' +# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem] +# +# When you package your gem with these options set RubyGems will automatically +# load your key and certificate from the secure paths. +# +# === Signed gems and security policies # # Now let's verify the signature. Go ahead and install the gem, but add the -# following options: "-P HighSecurity", like this: +# following options: <code>-P HighSecurity</code>, like this: # # # install the gem with using the security policy "HighSecurity" -# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# $ sudo gem install your.gem -P HighSecurity # -# The -P option sets your security policy -- we'll talk about that in just a -# minute. Eh, what's this? +# The <code>-P</code> option sets your security policy -- we'll talk about +# that in just a minute. Eh, what's this? # -# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem' -# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't -# verify data signature: Untrusted Signing Chain Root: cert = -# '/CN=gemmaster/DC=example/DC=com', error = 'path -# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem" -# does not exist' +# $ gem install -P HighSecurity your-gem-1.0.gem +# ERROR: While executing gem ... (Gem::Security::Exception) +# root cert /CN=you/DC=example is not trusted # # The culprit here is the security policy. RubyGems has several different # security policies. Let's take a short break and go over the security @@ -111,46 +120,48 @@ require 'fileutils' # RubyGems will simply refuse to install the package. Oh well, maybe # he'll have better luck causing problems for CPAN users instead :). # -# So, the reason RubyGems refused to install our shiny new signed gem was -# because it was from an untrusted source. Well, my code is infallible -# (hah!), so I'm going to add myself as a trusted source. -# -# Here's how: +# The reason RubyGems refused to install your shiny new signed gem was because +# it was from an untrusted source. Well, your code is infallible (naturally), +# so you need to add yourself as a trusted source: # -# # add trusted certificate -# gem cert --add gem-public_cert.pem +# # add trusted certificate +# gem cert --add ~/.gem/gem-public_cert.pem # -# I've added my public certificate as a trusted source. Now I can install -# packages signed my private key without any hassle. Let's try the install -# command above again: +# You've now added your public certificate as a trusted source. Now you can +# install packages signed by your private key without any hassle. Let's try +# the install command above again: # # # install the gem with using the HighSecurity policy (and this time # # without any shenanigans) -# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# $ gem install -P HighSecurity your-gem-1.0.gem +# Successfully installed your-gem-1.0 +# 1 gem installed # -# This time RubyGems should accept your signed package and begin installing. -# While you're waiting for RubyGems to work it's magic, have a look at some of -# the other security commands: +# This time RubyGems will accept your signed package and begin installing. # -# Usage: gem cert [options] +# While you're waiting for RubyGems to work it's magic, have a look at some of +# the other security commands by running <code>gem help cert</code>: # # Options: -# -a, --add CERT Add a trusted certificate. -# -l, --list List trusted certificates. -# -r, --remove STRING Remove trusted certificates containing STRING. -# -b, --build EMAIL_ADDR Build private key and self-signed certificate -# for EMAIL_ADDR. -# -C, --certificate CERT Certificate for --sign command. -# -K, --private-key KEY Private key for --sign command. -# -s, --sign NEWCERT Sign a certificate with my key and certificate. -# -# (By the way, you can pull up this list any time you'd like by typing "gem -# cert --help") -# -# Hmm. We've already covered the "--build" option, and the "--add", "--list", -# and "--remove" commands seem fairly straightforward; they allow you to add, -# list, and remove the certificates in your trusted certificate list. But -# what's with this "--sign" option? +# -a, --add CERT Add a trusted certificate. +# -l, --list [FILTER] List trusted certificates where the +# subject contains FILTER +# -r, --remove FILTER Remove trusted certificates where the +# subject contains FILTER +# -b, --build EMAIL_ADDR Build private key and self-signed +# certificate for EMAIL_ADDR +# -C, --certificate CERT Signing certificate for --sign +# -K, --private-key KEY Key for --sign or --build +# -s, --sign CERT Signs CERT with the key from -K +# and the certificate from -C +# +# We've already covered the <code>--build</code> option, and the +# <code>--add</code>, <code>--list</code>, and <code>--remove</code> commands +# seem fairly straightforward; they allow you to add, list, and remove the +# certificates in your trusted certificate list. But what's with this +# <code>--sign</code> option? +# +# === Certificate chains # # To answer that question, let's take a look at "certificate chains", a # concept I mentioned earlier. There are a couple of problems with @@ -172,134 +183,102 @@ require 'fileutils' # trust. Here's a hypothetical example of a trust hierarchy based (roughly) # on geography: # -# # -------------------------- -# | rubygems@rubyforge.org | +# | rubygems@rubygems.org | # -------------------------- # | # ----------------------------------- # | | # ---------------------------- ----------------------------- -# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com | +# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com | # ---------------------------- ----------------------------- # | | | | # --------------- ---------------- ----------- -------------- -# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc | +# | drbrain | | zenspider | | pabs@dc | | tomcope@dc | # --------------- ---------------- ----------- -------------- # # -# Now, rather than having 4 trusted certificates (one for alf@seattle, -# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1 -# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works: +# Now, rather than having 4 trusted certificates (one for drbrain, zenspider, +# pabs@dc, and tomecope@dc), a user could actually get by with one +# certificate, the "rubygems@rubygems.org" certificate. +# +# Here's how it works: +# +# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard +# of "drbrain", but his certificate has a valid signature from the +# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature +# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's +# much more reasonable for me to trust a package signed by "drbrain", because +# I can establish a chain to "rubygems@rubygems.org", which I do trust. # -# I install "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've -# never heard of "alf@seattle", but his certificate has a valid signature from -# the "seattle.rb@zenspider.com" certificate, which in turn has a valid -# signature from the "rubygems@rubyforge.org" certificate. Voila! At this -# point, it's much more reasonable for me to trust a package signed by -# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org", -# which I do trust. +# === Signing certificates # -# And the "--sign" option allows all this to happen. A developer creates -# their build certificate with the "--build" option, then has their -# certificate signed by taking it with them to their next regional Ruby meetup -# (in our hypothetical example), and it's signed there by the person holding -# the regional RubyGems signing certificate, which is signed at the next -# RubyConf by the holder of the top-level RubyGems certificate. At each point -# the issuer runs the same command: +# The <code>--sign</code> option allows all this to happen. A developer +# creates their build certificate with the <code>--build</code> option, then +# has their certificate signed by taking it with them to their next regional +# Ruby meetup (in our hypothetical example), and it's signed there by the +# person holding the regional RubyGems signing certificate, which is signed at +# the next RubyConf by the holder of the top-level RubyGems certificate. At +# each point the issuer runs the same command: # # # sign a certificate with the specified key and certificate # # (note that this modifies client_cert.pem!) # $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem # --sign client_cert.pem # -# Then the holder of issued certificate (in this case, our buddy -# "alf@seattle"), can start using this signed certificate to sign RubyGems. -# By the way, in order to let everyone else know about his new fancy signed -# certificate, "alf@seattle" would change his gemspec file to look like this: +# Then the holder of issued certificate (in this case, your buddy "drbrain"), +# can start using this signed certificate to sign RubyGems. By the way, in +# order to let everyone else know about his new fancy signed certificate, +# "drbrain" would save his newly signed certificate as +# <code>~/.gem/gem-public_cert.pem</code> # -# # signing key (still kept in an undisclosed location!) -# s.signing_key = '/mnt/floppy/alf-private_key.pem' -# -# # certificate chain (includes the issuer certificate now too) -# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem', -# '/home/alf/doc/alf_at_seattle-public_cert.pem'] -# -# Obviously, this RubyGems trust infrastructure doesn't exist yet. Also, in -# the "real world" issuers actually generate the child certificate from a +# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in +# the "real world", issuers actually generate the child certificate from a # certificate request, rather than sign an existing certificate. And our # hypothetical infrastructure is missing a certificate revocation system. # These are that can be fixed in the future... # -# I'm sure your new signed gem has finished installing by now (unless you're -# installing rails and all it's dependencies, that is ;D). At this point you -# should know how to do all of these new and interesting things: +# At this point you should know how to do all of these new and interesting +# things: # # * build a gem signing key and certificate -# * modify your existing gems to support signing # * adjust your security policy # * modify your trusted certificate list # * sign a certificate # -# If you've got any questions, feel free to contact me at the email address -# below. The next couple of sections -# -# -# == Command-Line Options -# -# Here's a brief summary of the certificate-related command line options: -# -# gem install -# -P, --trust-policy POLICY Specify gem trust policy. -# -# gem cert -# -a, --add CERT Add a trusted certificate. -# -l, --list List trusted certificates. -# -r, --remove STRING Remove trusted certificates containing -# STRING. -# -b, --build EMAIL_ADDR Build private key and self-signed -# certificate for EMAIL_ADDR. -# -C, --certificate CERT Certificate for --sign command. -# -K, --private-key KEY Private key for --sign command. -# -s, --sign NEWCERT Sign a certificate with my key and -# certificate. -# -# A more detailed description of each options is available in the walkthrough -# above. -# # == Manually verifying signatures # # In case you don't trust RubyGems you can verify gem signatures manually: # # 1. Fetch and unpack the gem # -# gem fetch some_signed_gem -# tar -xf some_signed_gem-1.0.gem +# gem fetch some_signed_gem +# tar -xf some_signed_gem-1.0.gem # # 2. Grab the public key from the gemspec # -# gem spec some_signed_gem-1.0.gem cert_chain | \ -# ruby -pe 'sub(/^ +/, "")' > public_key.crt +# gem spec some_signed_gem-1.0.gem cert_chain | \ +# ruby -ryaml -e 'puts YAML.load_documents($stdin)' > public_key.crt # # 3. Generate a SHA1 hash of the data.tar.gz # -# openssl dgst -sha1 < data.tar.gz > my.hash +# openssl dgst -sha1 < data.tar.gz > my.hash # # 4. Verify the signature # -# openssl rsautl -verify -inkey public_key.crt -certin \ -# -in data.tar.gz.sig > verified.hash +# openssl rsautl -verify -inkey public_key.crt -certin \ +# -in data.tar.gz.sig > verified.hash # # 5. Compare your hash to the verified hash # -# diff -s verified.hash my.hash +# diff -s verified.hash my.hash # # 6. Repeat 5 and 6 with metadata.gz # # == OpenSSL Reference # -# The .pem files generated by --build and --sign are just basic OpenSSL PEM -# files. Here's a couple of useful commands for manipulating them: +# The .pem files generated by --build and --sign are PEM files. Here's a +# couple of useful OpenSSL commands for manipulating them: # # # convert a PEM format X509 certificate into DER format: # # (note: Windows .cer files are X509 certificates in DER format) @@ -321,8 +300,8 @@ require 'fileutils' # * There's no way to define a system-wide trust list. # * custom security policies (from a YAML file, etc) # * Simple method to generate a signed certificate request -# * Support for OCSP, SCVP, CRLs, or some other form of cert -# status check (list is in order of preference) +# * Support for OCSP, SCVP, CRLs, or some other form of cert status check +# (list is in order of preference) # * Support for encrypted private keys # * Some sort of semi-formal trust hierarchy (see long-winded explanation # above) @@ -332,17 +311,13 @@ require 'fileutils' # MediumSecurity and HighSecurity policies) # * Better explanation of X509 naming (ie, we don't have to use email # addresses) -# * Possible alternate signing mechanisms (eg, via PGP). this could be done -# pretty easily by adding a :signing_type attribute to the gemspec, then add -# the necessary support in other places # * Honor AIA field (see note about OCSP above) -# * Maybe honor restriction extensions? +# * Honor extension restrictions # * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 -# file, instead of an array embedded in the metadata. ideas? -# * Possibly embed signature and key algorithms into metadata (right now -# they're assumed to be the same as what's set in Gem::Security::OPT) +# file, instead of an array embedded in the metadata. +# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1. # -# == About the Author +# == Original author # # Paul Duncan <pabs@pablotron.org> # http://pablotron.org/ @@ -355,472 +330,237 @@ module Gem::Security class Exception < Gem::Exception; end ## - # Default options for most of the methods below - - OPT = { - # private key options - :key_algo => Gem::SSL::PKEY_RSA, - :key_size => 2048, - - # public cert options - :cert_age => 365 * 24 * 3600, # 1 year - :dgst_algo => Gem::SSL::DIGEST_SHA1, - - # x509 certificate extensions - :cert_exts => { - 'basicConstraints' => 'CA:FALSE', - 'subjectKeyIdentifier' => 'hash', - 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature', - }, - - # save the key and cert to a file in build_self_signed_cert()? - :save_key => true, - :save_cert => true, - - # if you define either of these, then they'll be used instead of - # the output_fmt macro below - :save_key_path => nil, - :save_cert_path => nil, - - # output name format for self-signed certs - :output_fmt => 'gem-%s.pem', - :munge_re => Regexp.new(/[^a-z0-9_.-]+/), - - # output directory for trusted certificate checksums - :trust_dir => File.join(Gem.user_home, '.gem', 'trust'), - - # default permissions for trust directory and certs - :perms => { - :trust_dir => 0700, - :trusted_cert => 0600, - :signing_cert => 0600, - :signing_key => 0600, - }, - } + # Digest algorithm used to sign gems + + DIGEST_ALGORITHM = OpenSSL::Digest::SHA1 ## - # 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 below. - - class Policy - attr_accessor :verify_data, :verify_signer, :verify_chain, - :verify_root, :only_trusted, :only_signed - - # - # Create a new Gem::Security::Policy object with the given mode and - # options. - # - def initialize(policy = {}, opt = {}) - # set options - @opt = Gem::Security::OPT.merge(opt) - - # build policy - 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 + # Used internally to select the signing digest from all computed digests - # - # Get the path to the file for this cert. - # - def self.trusted_cert_path(cert, opt = {}) - opt = Gem::Security::OPT.merge(opt) + DIGEST_NAME = DIGEST_ALGORITHM.new.name # :nodoc: - # get digest algorithm, calculate checksum of root.subject - algo = opt[:dgst_algo] - dgst = algo.hexdigest(cert.subject.to_s) + ## + # Algorithm for creating the key pair used to sign gems - # build path to trusted cert file - name = "cert-#{dgst}.pem" + KEY_ALGORITHM = OpenSSL::PKey::RSA - # join and return path components - File::join(opt[:trust_dir], name) - end + ## + # Length of keys created by KEY_ALGORITHM - # - # Verify that the gem data with the given signature and signing chain - # matched this security policy at the specified time. - # - def verify_gem(signature, data, chain, time = Time.now) - Gem.ensure_ssl_available - cert_class = OpenSSL::X509::Certificate - exc = Gem::Security::Exception - chain ||= [] - - chain = chain.map{ |str| cert_class.new(str) } - signer, ch_len = chain[-1], chain.size - - # make sure signature is valid - if @verify_data - # get digest algorithm (TODO: this should be configurable) - dgst = @opt[:dgst_algo] - - # verify the data signature (this is the most important part, so don't - # screw it up :D) - v = signer.public_key.verify(dgst.new, signature, data) - raise exc, "Invalid Gem Signature" unless v - - # make sure the signer is valid - if @verify_signer - # make sure the signing cert is valid right now - v = signer.check_validity(nil, time) - raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid] - end - end - - # make sure the certificate chain is valid - if @verify_chain - # iterate down over the chain and verify each certificate against it's - # issuer - (ch_len - 1).downto(1) do |i| - issuer, cert = chain[i - 1, 2] - v = cert.check_validity(issuer, time) - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Invalid Signing Chain', cert.subject, v[:desc] - ] unless v[:is_valid] - end - - # verify root of chain - if @verify_root - # make sure root is self-signed - root = chain[0] - raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [ - 'Invalid Signing Chain Root', - 'Subject does not match Issuer for Gem Signing Chain', - root.subject.to_s, - root.issuer.to_s, - ] unless root.issuer.to_s == root.subject.to_s - - # make sure root is valid - v = root.check_validity(root, time) - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Invalid Signing Chain Root', root.subject, v[:desc] - ] unless v[:is_valid] - - # verify that the chain root is trusted - if @only_trusted - # get digest algorithm, calculate checksum of root.subject - algo = @opt[:dgst_algo] - path = Gem::Security::Policy.trusted_cert_path(root, @opt) - - # check to make sure trusted path exists - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Untrusted Signing Chain Root', - root.subject.to_s, - "path \"#{path}\" does not exist", - ] unless File.exist?(path) - - # load calculate digest from saved cert file - save_cert = OpenSSL::X509::Certificate.new(File.read(path)) - save_dgst = algo.digest(save_cert.public_key.to_s) - - # create digest of public key - pkey_str = root.public_key.to_s - cert_dgst = algo.digest(pkey_str) - - # now compare the two digests, raise exception - # if they don't match - raise exc, "%s: %s (saved = '%s', root = '%s')" % [ - 'Invalid Signing Chain Root', - "Saved checksum doesn't match root checksum", - save_dgst, cert_dgst, - ] unless save_dgst == cert_dgst - end - end - - # return the signing chain - chain.map { |cert| cert.subject } - end - end - end + KEY_LENGTH = 2048 ## - # No security policy: all package signature checks are disabled. + # One year in seconds - NoSecurity = Policy.new( - :verify_data => false, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + ONE_YEAR = 86400 * 365 ## - # 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. + # The default set of extensions are: # - # This policy is basically useless. better than nothing, but can still be - # easily spoofed, and is not recommended. - - AlmostNoSecurity = Policy.new( - :verify_data => true, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + # * The certificate is not a certificate authority + # * The key for the certificate may be used for key and data encipherment + # and digital signatures + # * The certificate contains a subject key identifier + + EXTENSIONS = { + 'basicConstraints' => 'CA:FALSE', + 'keyUsage' => + 'keyEncipherment,dataEncipherment,digitalSignature', + 'subjectKeyIdentifier' => 'hash', + } - ## - # 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( - :verify_data => true, - :verify_signer => true, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + def self.alt_name_or_x509_entry certificate, x509_entry + alt_name = certificate.extensions.find do |extension| + extension.oid == "#{x509_entry}AltName" + end - ## - # 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( - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => false - ) + return alt_name.value if alt_name + + certificate.send x509_entry + end ## - # 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. + # Creates an unsigned certificate for +subject+ and +key+. The lifetime of + # the key is from the current time to +age+ which defaults to one year. # - # 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( - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => true - ) + # The +extensions+ restrict the key to the indicated uses. - ## - # Hash of configured security policies - - Policies = { - 'NoSecurity' => NoSecurity, - 'AlmostNoSecurity' => AlmostNoSecurity, - 'LowSecurity' => LowSecurity, - 'MediumSecurity' => MediumSecurity, - 'HighSecurity' => HighSecurity, - } + def self.create_cert subject, key, age = ONE_YEAR, extensions = EXTENSIONS, + serial = 1 + cert = OpenSSL::X509::Certificate.new - ## - # Sign the cert cert with @signing_key and @signing_cert, using the digest - # algorithm opt[:dgst_algo]. Returns the newly signed certificate. + cert.public_key = key.public_key + cert.version = 2 + cert.serial = serial - def self.sign_cert(cert, signing_key, signing_cert, opt = {}) - opt = OPT.merge(opt) + cert.not_before = Time.now + cert.not_after = Time.now + age - cert.issuer = signing_cert.subject - cert.sign signing_key, opt[:dgst_algo].new + cert.subject = subject - cert - end + ef = OpenSSL::X509::ExtensionFactory.new nil, cert - ## - # 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 self.verify_trust_dir(path, perms) - # if the directory exists, then make sure it is in fact a directory. if - # it doesn't exist, then create it with the appropriate permissions - if File.exist?(path) - # verify that the trust directory is actually a directory - unless File.directory?(path) - err = "trust directory #{path} isn't a directory" - raise Gem::Security::Exception, err - end - else - # trust directory doesn't exist, so create it with permissions - FileUtils.mkdir_p(path) - FileUtils.chmod(perms, path) + cert.extensions = extensions.map do |ext_name, value| + ef.create_extension ext_name, value end + + cert end ## - # Build a certificate from the given DN and private key. + # Creates a self-signed certificate with an issuer and subject from +email+, + # a subject alternative name of +email+ and the given +extensions+ for the + # +key+. - def self.build_cert(name, key, opt = {}) - Gem.ensure_ssl_available - opt = OPT.merge opt + def self.create_cert_email email, key, age = ONE_YEAR, extensions = EXTENSIONS + subject = email_to_name email - cert = OpenSSL::X509::Certificate.new + extensions = extensions.merge "subjectAltName" => "email:#{email}" - cert.not_after = Time.now + opt[:cert_age] - cert.not_before = Time.now - cert.public_key = key.public_key - cert.serial = 0 - cert.subject = name - cert.version = 2 + create_cert_self_signed subject, key, age, extensions + end - ef = OpenSSL::X509::ExtensionFactory.new nil, cert + ## + # Creates a self-signed certificate with an issuer and subject of +subject+ + # and the given +extensions+ for the +key+. - cert.extensions = opt[:cert_exts].map do |ext_name, value| - ef.create_extension ext_name, value - end + def self.create_cert_self_signed subject, key, age = ONE_YEAR, + extensions = EXTENSIONS, serial = 1 + certificate = create_cert subject, key, age, extensions - i_key = opt[:issuer_key] || key - i_cert = opt[:issuer_cert] || cert + sign certificate, key, certificate, age, extensions, serial + end - cert = sign_cert cert, i_key, i_cert, opt + ## + # Creates a new key pair of the specified +length+ and +algorithm+. The + # default is a 2048 bit RSA key. - cert + def self.create_key length = KEY_LENGTH, algorithm = KEY_ALGORITHM + algorithm.new length end ## - # Build a self-signed certificate for the given email address. + # Turns +email_address+ into an OpenSSL::X509::Name - def self.build_self_signed_cert(email_addr, opt = {}) - Gem.ensure_ssl_available - opt = OPT.merge(opt) - path = { :key => nil, :cert => nil } + def self.email_to_name email_address + email_address = email_address.gsub(/[^\w@.-]+/i, '_') - name = email_to_name email_addr, opt[:munge_re] + cn, dcs = email_address.split '@' - key = opt[:key_algo].new opt[:key_size] + dcs = dcs.split '.' - verify_trust_dir opt[:trust_dir], opt[:perms][:trust_dir] + name = "CN=#{cn}/#{dcs.map { |dc| "DC=#{dc}" }.join '/'}" - if opt[:save_key] then - path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key') + OpenSSL::X509::Name.parse name + end - open path[:key], 'wb' do |io| - io.chmod opt[:perms][:signing_key] - io.write key.to_pem - end + ## + # Signs +expired_certificate+ with +private_key+ if the keys match and the + # expired certificate was self-signed. + #-- + # TODO increment serial + + def self.re_sign expired_certificate, private_key, age = ONE_YEAR, + extensions = EXTENSIONS + raise Gem::Security::Exception, + "incorrect signing key for re-signing " \ + "#{expired_certificate.subject}" unless + expired_certificate.public_key.to_pem == private_key.public_key.to_pem + + unless expired_certificate.subject.to_s == + expired_certificate.issuer.to_s then + subject = alt_name_or_x509_entry expired_certificate, :subject + issuer = alt_name_or_x509_entry expired_certificate, :issuer + + raise Gem::Security::Exception, + "#{subject} is not self-signed, contact #{issuer} " \ + "to obtain a valid certificate" end - cert = build_cert name, key, opt + serial = expired_certificate.serial + 1 - if opt[:save_cert] then - path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert') + create_cert_self_signed(expired_certificate.subject, private_key, age, + extensions, serial) + end - open path[:cert], 'wb' do |file| - file.chmod opt[:perms][:signing_cert] - file.write cert.to_pem - end - end + ## + # Resets the trust directory for verifying gems. - { :key => key, :cert => cert, - :key_path => path[:key], :cert_path => path[:cert] } + def self.reset + @trust_dir = nil end ## - # Turns +email_address+ into an OpenSSL::X509::Name + # Sign the public key from +certificate+ with the +signing_key+ and + # +signing_cert+, using the Gem::Security::DIGEST_ALGORITHM. Uses the + # default certificate validity range and extensions. + # + # Returns the newly signed certificate. - def self.email_to_name email_address, munge_re - cn, dcs = email_address.split '@' + def self.sign certificate, signing_key, signing_cert, + age = ONE_YEAR, extensions = EXTENSIONS, serial = 1 + signee_subject = certificate.subject + signee_key = certificate.public_key - dcs = dcs.split '.' + alt_name = certificate.extensions.find do |extension| + extension.oid == 'subjectAltName' + end - cn = cn.gsub munge_re, '_' + extensions = extensions.merge 'subjectAltName' => alt_name.value if + alt_name - dcs = dcs.map do |dc| - dc.gsub munge_re, '_' + issuer_alt_name = signing_cert.extensions.find do |extension| + extension.oid == 'subjectAltName' end - name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/') + extensions = extensions.merge 'issuerAltName' => issuer_alt_name.value if + issuer_alt_name - OpenSSL::X509::Name.parse name + signed = create_cert signee_subject, signee_key, age, extensions, serial + signed.issuer = signing_cert.subject + + signed.sign signing_key, Gem::Security::DIGEST_ALGORITHM.new end ## - # Add certificate to trusted cert list. - # - # Note: At the moment these are stored in OPT[:trust_dir], although that - # directory may change in the future. + # Returns a Gem::Security::TrustDir which wraps the directory where trusted + # certificates live. - def self.add_trusted_cert(cert, opt = {}) - opt = OPT.merge(opt) + def self.trust_dir + return @trust_dir if @trust_dir - # get destination path - path = Gem::Security::Policy.trusted_cert_path(cert, opt) + dir = File.join Gem.user_home, '.gem', 'trust' - # verify trust directory (can't write to nowhere, you know) - verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + @trust_dir ||= Gem::Security::TrustDir.new dir + end - # write cert to output file - File.open(path, 'wb') do |file| - file.chmod(opt[:perms][:trusted_cert]) - file.write(cert.to_pem) - end + ## + # Enumerates the trusted certificates via Gem::Security::TrustDir. - # return nil - nil + def self.trusted_certificates &block + trust_dir.each_certificate(&block) end ## - # Basic OpenSSL-based package signing class. - - class Signer - - attr_accessor :cert_chain - attr_accessor :key - - def initialize(key, cert_chain) - Gem.ensure_ssl_available - @algo = Gem::Security::OPT[:dgst_algo] - @key, @cert_chain = key, cert_chain - - # check key, if it's a file, and if it's key, leave it alone - if @key && !@key.kind_of?(OpenSSL::PKey::PKey) - @key = OpenSSL::PKey::RSA.new(File.read(@key)) - end - - # check cert chain, if it's a file, load it, if it's cert data, convert - # it into a cert object, and if it's a cert object, leave it alone - if @cert_chain - @cert_chain = @cert_chain.map do |cert| - # check cert, if it's a file, load it, if it's cert data, convert it - # into a cert object, and if it's a cert object, leave it alone - if cert && !cert.kind_of?(OpenSSL::X509::Certificate) - cert = File.read(cert) if File::exist?(cert) - cert = OpenSSL::X509::Certificate.new(cert) - end - cert - end - end - end + # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given + # +permissions+. - ## - # Sign data with given digest algorithm + def self.write pemmable, path, permissions = 0600 + path = File.expand_path path - def sign(data) - @key.sign(@algo.new, data) + open path, 'wb', permissions do |io| + io.write pemmable.to_pem end + path end + reset + end +require 'rubygems/security/policy' +require 'rubygems/security/policies' +require 'rubygems/security/signer' +require 'rubygems/security/trust_dir' + |