Class: Gem::Security::Signer

Inherits:
Object
  • Object
show all
Includes:
UserInteraction
Defined in:
lib/rubygems/security/signer.rb

Constant Summary collapse

DEFAULT_OPTIONS =
{
  expiration_length_days: 365
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from UserInteraction

#alert, #alert_error, #alert_warning, #ask, #ask_for_password, #ask_yes_no, #choose_from_list, #say, #terminate_interaction, #verbose

Methods included from DefaultUserInteraction

ui, #ui, #ui=, ui=, use_ui, #use_ui

Methods included from Text

#clean_text, #format_text, #levenshtein_distance, #min3, #truncate_text

Constructor Details

#initialize(key, cert_chain, passphrase = nil, options = {}) ⇒ Signer

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.


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/rubygems/security/signer.rb', line 68

def initialize(key, cert_chain, passphrase = nil, options = {})
  @cert_chain = cert_chain
  @key        = key
  @passphrase = passphrase
  @options = DEFAULT_OPTIONS.merge(options)

  unless @key
    default_key = File.join Gem.default_key_path
    @key = default_key if File.exist? default_key
  end

  unless @cert_chain
    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

  if @key && !@key.is_a?(OpenSSL::PKey::RSA)
    @key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase)
  end

  if @cert_chain
    @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

Instance Attribute Details

#cert_chainObject

The chain of certificates for signing including the signing certificate


14
15
16
# File 'lib/rubygems/security/signer.rb', line 14

def cert_chain
  @cert_chain
end

#digest_algorithmObject (readonly)

The digest algorithm used to create the signature


24
25
26
# File 'lib/rubygems/security/signer.rb', line 24

def digest_algorithm
  @digest_algorithm
end

#digest_nameObject (readonly)

The name of the digest algorithm, used to pull digests out of the hash by name.


30
31
32
# File 'lib/rubygems/security/signer.rb', line 30

def digest_name
  @digest_name
end

#keyObject

The private key for the signing certificate


19
20
21
# File 'lib/rubygems/security/signer.rb', line 19

def key
  @key
end

#optionsObject (readonly)

Gem::Security::Signer options


35
36
37
# File 'lib/rubygems/security/signer.rb', line 35

def options
  @options
end

Class Method Details

.re_sign_cert(expired_cert, expired_cert_path, private_key) {|expired_cert_path, new_expired_cert_path| ... } ⇒ Object

Attemps to re-sign an expired cert with a given private key

Yields:

  • (expired_cert_path, new_expired_cert_path)

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rubygems/security/signer.rb', line 43

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)

  re_signed_cert = Gem::Security.re_sign(
    expired_cert,
    private_key,
    (Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days)
  )

  Gem::Security.write(re_signed_cert, expired_cert_path)

  yield(expired_cert_path, new_expired_cert_path) if block_given?
end

Instance Method Details

#extract_name(cert) ⇒ Object

Extracts the full name of cert. If the certificate has a subjectAltName this value is preferred, otherwise the subject is used.


108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rubygems/security/signer.rb', line 108

def extract_name(cert) # :nodoc:
  subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid }

  if subject_alt_name
    /\Aemail:/ =~ subject_alt_name.value

    $' || subject_alt_name.value
  else
    cert.subject
  end
end

#load_cert_chainObject

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.


125
126
127
128
129
130
131
132
133
134
135
# File 'lib/rubygems/security/signer.rb', line 125

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

#re_sign_key(expiration_length: Gem::Security::ONE_YEAR) ⇒ Object

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 and the private key is saved at ~/.gem/gem-private_key.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-public_cert.pem.expired.%Y%m%d%H%M%S where the expiry time (not after) is used for the timestamp.


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/rubygems/security/signer.rb', line 174

def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :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_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')
    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)
      Gem::Security.write(old_cert, old_cert_path)

      cert = Gem::Security.re_sign(old_cert, @key, expiration_length)

      Gem::Security.write(cert, disk_cert_path)

      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}")

      @cert_chain = [cert]
    end
  end
end

#sign(data) ⇒ Object

Sign data with given digest algorithm


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rubygems/security/signer.rb', line 140

def sign(data)
  return unless @key

  raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty?

  if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now
    alert("Your certificate has expired, trying to re-sign it...")

    re_sign_key(
      expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
    )
  end

  full_name = extract_name @cert_chain.last

  Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name

  @key.sign @digest_algorithm.new, data
end