2016-02-01 07:43:26 -05:00
|
|
|
# frozen_string_literal: true
|
2012-11-29 01:52:18 -05:00
|
|
|
##
|
|
|
|
# Basic OpenSSL-based package signing class.
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
require "rubygems/user_interaction"
|
|
|
|
|
2012-11-29 01:52:18 -05:00
|
|
|
class Gem::Security::Signer
|
2018-08-27 06:05:04 -04:00
|
|
|
include Gem::UserInteraction
|
|
|
|
|
2012-11-29 01:52:18 -05:00
|
|
|
##
|
|
|
|
# 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:
|
|
|
|
|
2018-10-21 20:27:02 -04:00
|
|
|
##
|
|
|
|
# Gem::Security::Signer options
|
|
|
|
|
|
|
|
attr_reader :options
|
|
|
|
|
|
|
|
DEFAULT_OPTIONS = {
|
|
|
|
expiration_length_days: 365
|
|
|
|
}.freeze
|
|
|
|
|
2018-09-18 04:37:18 -04:00
|
|
|
##
|
2020-01-31 21:14:04 -05:00
|
|
|
# Attempts to re-sign an expired cert with a given private key
|
2018-09-18 04:37:18 -04:00
|
|
|
def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
|
|
|
|
return unless expired_cert.not_after < Time.now
|
|
|
|
|
|
|
|
expiry = expired_cert.not_after.strftime('%Y%m%d%H%M%S')
|
|
|
|
expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
|
|
|
|
new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
|
|
|
|
|
|
|
|
Gem::Security.write(expired_cert, new_expired_cert_path)
|
|
|
|
|
2018-10-21 20:27:02 -04:00
|
|
|
re_signed_cert = Gem::Security.re_sign(
|
|
|
|
expired_cert,
|
|
|
|
private_key,
|
|
|
|
(Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days)
|
|
|
|
)
|
2018-09-18 04:37:18 -04:00
|
|
|
|
|
|
|
Gem::Security.write(re_signed_cert, expired_cert_path)
|
|
|
|
|
|
|
|
yield(expired_cert_path, new_expired_cert_path) if block_given?
|
|
|
|
end
|
|
|
|
|
2012-11-29 01:52:18 -05:00
|
|
|
##
|
|
|
|
# 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.
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
def initialize(key, cert_chain, passphrase = nil, options = {})
|
2012-11-29 01:52:18 -05:00
|
|
|
@cert_chain = cert_chain
|
|
|
|
@key = key
|
2018-08-27 06:05:04 -04:00
|
|
|
@passphrase = passphrase
|
2018-10-21 20:27:02 -04:00
|
|
|
@options = DEFAULT_OPTIONS.merge(options)
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless @key
|
2019-02-14 07:59:03 -05:00
|
|
|
default_key = File.join Gem.default_key_path
|
2012-11-29 01:52:18 -05:00
|
|
|
@key = default_key if File.exist? default_key
|
|
|
|
end
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
unless @cert_chain
|
2013-06-04 17:54:58 -04:00
|
|
|
default_cert = File.join Gem.default_cert_path
|
2012-11-29 01:52:18 -05:00
|
|
|
@cert_chain = [default_cert] if File.exist? default_cert
|
|
|
|
end
|
|
|
|
|
|
|
|
@digest_algorithm = Gem::Security::DIGEST_ALGORITHM
|
|
|
|
@digest_name = Gem::Security::DIGEST_NAME
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
if @key && !@key.is_a?(OpenSSL::PKey::RSA)
|
|
|
|
@key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase)
|
|
|
|
end
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if @cert_chain
|
2012-11-29 01:52:18 -05:00
|
|
|
@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
|
|
|
|
|
2013-09-14 04:59:02 -04:00
|
|
|
##
|
|
|
|
# Extracts the full name of +cert+. If the certificate has a subjectAltName
|
|
|
|
# this value is preferred, otherwise the subject is used.
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
def extract_name(cert) # :nodoc:
|
2020-06-10 13:46:05 -04:00
|
|
|
subject_alt_name = cert.extensions.find {|e| 'subjectAltName' == e.oid }
|
2013-09-14 04:59:02 -04:00
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if subject_alt_name
|
2020-04-19 16:39:42 -04:00
|
|
|
/\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith
|
2013-09-14 04:59:02 -04:00
|
|
|
|
|
|
|
$' || subject_alt_name.value
|
|
|
|
else
|
|
|
|
cert.subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-11-29 01:52:18 -05:00
|
|
|
##
|
|
|
|
# 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
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
def sign(data)
|
2012-11-29 01:52:18 -05:00
|
|
|
return unless @key
|
|
|
|
|
2016-06-18 01:11:55 -04:00
|
|
|
raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty?
|
|
|
|
|
2018-11-21 05:20:47 -05:00
|
|
|
if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now
|
2019-04-26 07:26:21 -04:00
|
|
|
alert("Your certificate has expired, trying to re-sign it...")
|
|
|
|
|
2018-10-21 20:27:02 -04:00
|
|
|
re_sign_key(
|
|
|
|
expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
|
|
|
|
)
|
2012-11-29 01:52:18 -05:00
|
|
|
end
|
|
|
|
|
2013-09-14 04:59:02 -04:00
|
|
|
full_name = extract_name @cert_chain.last
|
|
|
|
|
|
|
|
Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
|
2012-11-29 01:52:18 -05:00
|
|
|
|
|
|
|
@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
|
2018-08-27 06:05:04 -04:00
|
|
|
# and the private key is saved at ~/.gem/gem-private_key.pem
|
2012-11-29 01:52:18 -05:00
|
|
|
# * 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
|
2015-01-09 09:20:10 -05:00
|
|
|
# be saved as ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S where the
|
2012-11-29 01:52:18 -05:00
|
|
|
# expiry time (not after) is used for the timestamp.
|
|
|
|
|
2018-10-21 20:27:02 -04:00
|
|
|
def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc:
|
2012-11-29 01:52:18 -05:00
|
|
|
old_cert = @cert_chain.last
|
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
disk_cert_path = File.join(Gem.default_cert_path)
|
|
|
|
disk_cert = File.read(disk_cert_path) rescue nil
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
disk_key_path = File.join(Gem.default_key_path)
|
|
|
|
disk_key =
|
|
|
|
OpenSSL::PKey::RSA.new(File.read(disk_key_path), @passphrase) rescue nil
|
|
|
|
|
|
|
|
return unless disk_key
|
|
|
|
|
|
|
|
if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem
|
|
|
|
expiry = old_cert.not_after.strftime('%Y%m%d%H%M%S')
|
2012-11-29 01:52:18 -05:00
|
|
|
old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
|
2018-08-27 06:05:04 -04:00
|
|
|
old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file)
|
|
|
|
|
|
|
|
unless File.exist?(old_cert_path)
|
|
|
|
Gem::Security.write(old_cert, old_cert_path)
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-10-21 20:27:02 -04:00
|
|
|
cert = Gem::Security.re_sign(old_cert, @key, expiration_length)
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
Gem::Security.write(cert, disk_cert_path)
|
2012-11-29 01:52:18 -05:00
|
|
|
|
2018-08-27 06:05:04 -04:00
|
|
|
alert("Your cert: #{disk_cert_path} has been auto re-signed with the key: #{disk_key_path}")
|
|
|
|
alert("Your expired cert will be located at: #{old_cert_path}")
|
2012-11-29 01:52:18 -05:00
|
|
|
|
|
|
|
@cert_chain = [cert]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|