1##
2# A Gem::Security::Policy object encapsulates the settings for verifying
3# signed gem files.  This is the base class.  You can either declare an
4# instance of this or use one of the preset security policies in
5# Gem::Security::Policies.
6
7class Gem::Security::Policy
8
9  attr_reader :name
10
11  attr_accessor :only_signed
12  attr_accessor :only_trusted
13  attr_accessor :verify_chain
14  attr_accessor :verify_data
15  attr_accessor :verify_root
16  attr_accessor :verify_signer
17
18  ##
19  # Create a new Gem::Security::Policy object with the given mode and
20  # options.
21
22  def initialize name, policy = {}, opt = {}
23    require 'openssl'
24
25    @name = name
26
27    @opt = opt
28
29    # Default to security
30    @only_signed   = true
31    @only_trusted  = true
32    @verify_chain  = true
33    @verify_data   = true
34    @verify_root   = true
35    @verify_signer = true
36
37    policy.each_pair do |key, val|
38      case key
39      when :verify_data   then @verify_data   = val
40      when :verify_signer then @verify_signer = val
41      when :verify_chain  then @verify_chain  = val
42      when :verify_root   then @verify_root   = val
43      when :only_trusted  then @only_trusted  = val
44      when :only_signed   then @only_signed   = val
45      end
46    end
47  end
48
49  ##
50  # Verifies each certificate in +chain+ has signed the following certificate
51  # and is valid for the given +time+.
52
53  def check_chain chain, time
54    raise Gem::Security::Exception, 'missing signing chain' unless chain
55    raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
56
57    begin
58      chain.each_cons 2 do |issuer, cert|
59        check_cert cert, issuer, time
60      end
61
62      true
63    rescue Gem::Security::Exception => e
64      raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
65    end
66  end
67
68  ##
69  # Verifies that +data+ matches the +signature+ created by +public_key+ and
70  # the +digest+ algorithm.
71
72  def check_data public_key, digest, signature, data
73    raise Gem::Security::Exception, "invalid signature" unless
74      public_key.verify digest.new, signature, data.digest
75
76    true
77  end
78
79  ##
80  # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
81  # If the +issuer+ is +nil+ no verification is performed.
82
83  def check_cert signer, issuer, time
84    raise Gem::Security::Exception, 'missing signing certificate' unless
85      signer
86
87    message = "certificate #{signer.subject}"
88
89    if not_before = signer.not_before and not_before > time then
90      raise Gem::Security::Exception,
91            "#{message} not valid before #{not_before}"
92    end
93
94    if not_after = signer.not_after and not_after < time then
95      raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
96    end
97
98    if issuer and not signer.verify issuer.public_key then
99      raise Gem::Security::Exception,
100            "#{message} was not issued by #{issuer.subject}"
101    end
102
103    true
104  end
105
106  ##
107  # Ensures the public key of +key+ matches the public key in +signer+
108
109  def check_key signer, key
110    unless signer and key then
111      return true unless @only_signed
112
113      raise Gem::Security::Exception, 'missing key or signature'
114    end
115
116    raise Gem::Security::Exception,
117      "certificate #{signer.subject} does not match the signing key" unless
118        signer.public_key.to_pem == key.public_key.to_pem
119
120    true
121  end
122
123  ##
124  # Ensures the root certificate in +chain+ is self-signed and valid for
125  # +time+.
126
127  def check_root chain, time
128    raise Gem::Security::Exception, 'missing signing chain' unless chain
129
130    root = chain.first
131
132    raise Gem::Security::Exception, 'missing root certificate' unless root
133
134    raise Gem::Security::Exception,
135          "root certificate #{root.subject} is not self-signed " +
136          "(issuer #{root.issuer})" if
137      root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
138
139    check_cert root, root, time
140  end
141
142  ##
143  # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
144  # the digests of the two certificates match according to +digester+
145
146  def check_trust chain, digester, trust_dir
147    raise Gem::Security::Exception, 'missing signing chain' unless chain
148
149    root = chain.first
150
151    raise Gem::Security::Exception, 'missing root certificate' unless root
152
153    path = Gem::Security.trust_dir.cert_path root
154
155    unless File.exist? path then
156      message = "root cert #{root.subject} is not trusted"
157
158      message << " (root of signing cert #{chain.last.subject})" if
159        chain.length > 1
160
161      raise Gem::Security::Exception, message
162    end
163
164    save_cert = OpenSSL::X509::Certificate.new File.read path
165    save_dgst = digester.digest save_cert.public_key.to_s
166
167    pkey_str = root.public_key.to_s
168    cert_dgst = digester.digest pkey_str
169
170    raise Gem::Security::Exception,
171          "trusted root certificate #{root.subject} checksum " +
172          "does not match signing root certificate checksum" unless
173      save_dgst == cert_dgst
174
175    true
176  end
177
178  def inspect # :nodoc:
179    ("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
180     "signed-only: %p trusted-only: %p]") % [
181      @name, @verify_chain, @verify_data, @verify_root, @verify_signer,
182      @only_signed, @only_trusted,
183    ]
184  end
185
186  ##
187  # Verifies the certificate +chain+ is valid, the +digests+ match the
188  # signatures +signatures+ created by the signer depending on the +policy+
189  # settings.
190  #
191  # If +key+ is given it is used to validate the signing certificate.
192
193  def verify chain, key = nil, digests = {}, signatures = {}
194    if @only_signed and signatures.empty? then
195      raise Gem::Security::Exception,
196        "unsigned gems are not allowed by the #{name} policy"
197    end
198
199    opt       = @opt
200    digester  = Gem::Security::DIGEST_ALGORITHM
201    trust_dir = opt[:trust_dir]
202    time      = Time.now
203
204    _, signer_digests = digests.find do |algorithm, file_digests|
205      file_digests.values.first.name == Gem::Security::DIGEST_NAME
206    end
207
208    if @verify_data then
209      raise Gem::Security::Exception, 'no digests provided (probable bug)' if
210        signer_digests.nil? or signer_digests.empty?
211    else
212      signer_digests = {}
213    end
214
215    signer = chain.last
216
217    check_key signer, key if key
218
219    check_cert signer, nil, time if @verify_signer
220
221    check_chain chain, time if @verify_chain
222
223    check_root chain, time if @verify_root
224
225    check_trust chain, digester, trust_dir if @only_trusted
226
227    signatures.each do |file, _|
228      digest = signer_digests[file]
229
230      raise Gem::Security::Exception, "missing digest for #{file}" unless
231        digest
232    end
233
234    signer_digests.each do |file, digest|
235      signature = signatures[file]
236
237      raise Gem::Security::Exception, "missing signature for #{file}" unless
238        signature
239
240      check_data signer.public_key, digester, signature, digest if @verify_data
241    end
242
243    true
244  end
245
246  ##
247  # Extracts the certificate chain from the +spec+ and calls #verify to ensure
248  # the signatures and certificate chain is valid according to the policy..
249
250  def verify_signatures spec, digests, signatures
251    chain = spec.cert_chain.map do |cert_pem|
252      OpenSSL::X509::Certificate.new cert_pem
253    end
254
255    verify chain, nil, digests, signatures
256
257    true
258  end
259
260  alias to_s name # :nodoc:
261
262end
263
264