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.



67
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
# File 'lib/rubygems/security/signer.rb', line 67

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_name      = Gem::Security::DIGEST_NAME
  @digest_algorithm = Gem::Security.create_digest(@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



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

def cert_chain
  @cert_chain
end

#digest_algorithmObject (readonly)

The digest algorithm used to create the signature



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

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.



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

def digest_name
  @digest_name
end

#keyObject

The private key for the signing certificate



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

def key
  @key
end

#optionsObject (readonly)

Gem::Security::Signer options



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

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

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

Yields:

  • (expired_cert_path, new_expired_cert_path)


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

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.



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

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

  if subject_alt_name
    /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith

    $' || 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.



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

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.



173
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
# File 'lib/rubygems/security/signer.rb', line 173

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



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

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