summaryrefslogtreecommitdiff
path: root/lib/rubygems/security/signer.rb
blob: a6d0161edcbdfb5128f1275cd25a33a27362d3aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# frozen_string_literal: false
##
# Basic OpenSSL-based package signing class.

class Gem::Security::Signer

  ##
  # 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:

  ##
  # 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.

  def initialize key, cert_chain, passphrase = nil
    @cert_chain = cert_chain
    @key        = key

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

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

    @key = OpenSSL::PKey::RSA.new File.read(@key), passphrase if
      @key and not OpenSSL::PKey::RSA === @key

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

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

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

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

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

  ##
  # 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

  def sign data
    return unless @key

    if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now then
      re_sign_key
    end

    full_name = extract_name @cert_chain.last

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

    @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
  # * 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.

  def re_sign_key # :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  =
      File.read File.join(Gem.default_key_path) rescue nil

    if disk_key == @key.to_pem and disk_cert == old_cert.to_pem then
      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 then
        Gem::Security.write old_cert, old_cert_path

        cert = Gem::Security.re_sign old_cert, @key

        Gem::Security.write cert, disk_cert_path

        @cert_chain = [cert]
      end
    end
  end

end