summaryrefslogtreecommitdiff
path: root/lib/rubygems/security.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/security.rb')
-rw-r--r--lib/rubygems/security.rb850
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'
+