~ubuntu-branches/ubuntu/precise/puppet/precise-security

« back to all changes in this revision

Viewing changes to .pc/2.7.17-Puppet-July-2012-CVE-fixes.patch/lib/puppet/ssl/certificate_authority.rb

  • Committer: Package Import Robot
  • Author(s): Marc Deslauriers
  • Date: 2012-07-10 07:58:03 UTC
  • Revision ID: package-import@ubuntu.com-20120710075803-og8iubg2a90dtk7f
Tags: 2.7.11-1ubuntu2.1
* SECURITY UPDATE: Multiple July 2012 security issues
  - debian/patches/2.7.17-Puppet-July-2012-CVE-fixes.patch: upstream
    patch to fix multiple security issues.
  - CVE-2012-3864: arbitrary file read on master from authenticated
    clients
  - CVE-2012-3865: arbitrary file delete or denial of service on master
    from authenticated clients
  - CVE-2012-3866: last_run_report.yaml report file is world readable and
    leads to arbitrary file read on master by an agent
  - CVE-2012-3867: insufficient input validation for agent cert hostnames

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'monitor'
 
2
require 'puppet/ssl/host'
 
3
require 'puppet/ssl/certificate_request'
 
4
 
 
5
# The class that knows how to sign certificates.  It creates
 
6
# a 'special' SSL::Host whose name is 'ca', thus indicating
 
7
# that, well, it's the CA.  There's some magic in the
 
8
# indirector/ssl_file terminus base class that does that
 
9
# for us.
 
10
#   This class mostly just signs certs for us, but
 
11
# it can also be seen as a general interface into all of the
 
12
# SSL stuff.
 
13
class Puppet::SSL::CertificateAuthority
 
14
  # We will only sign extensions on this whitelist, ever.  Any CSR with a
 
15
  # requested extension that we don't recognize is rejected, against the risk
 
16
  # that it will introduce some security issue through our ignorance of it.
 
17
  #
 
18
  # Adding an extension to this whitelist simply means we will consider it
 
19
  # further, not that we will always accept a certificate with an extension
 
20
  # requested on this list.
 
21
  RequestExtensionWhitelist = %w{subjectAltName}
 
22
 
 
23
  require 'puppet/ssl/certificate_factory'
 
24
  require 'puppet/ssl/inventory'
 
25
  require 'puppet/ssl/certificate_revocation_list'
 
26
  require 'puppet/ssl/certificate_authority/interface'
 
27
  require 'puppet/network/authstore'
 
28
 
 
29
  extend MonitorMixin
 
30
 
 
31
  class CertificateVerificationError < RuntimeError
 
32
    attr_accessor :error_code
 
33
 
 
34
    def initialize(code)
 
35
      @error_code = code
 
36
    end
 
37
  end
 
38
 
 
39
  def self.singleton_instance
 
40
    synchronize do
 
41
      @singleton_instance ||= new
 
42
    end
 
43
  end
 
44
 
 
45
  class CertificateSigningError < RuntimeError
 
46
    attr_accessor :host
 
47
 
 
48
    def initialize(host)
 
49
      @host = host
 
50
    end
 
51
  end
 
52
 
 
53
  def self.ca?
 
54
    return false unless Puppet[:ca]
 
55
    return false unless Puppet.run_mode.master?
 
56
    true
 
57
  end
 
58
 
 
59
  # If this process can function as a CA, then return a singleton
 
60
  # instance.
 
61
  def self.instance
 
62
    return nil unless ca?
 
63
 
 
64
    singleton_instance
 
65
  end
 
66
 
 
67
  attr_reader :name, :host
 
68
 
 
69
  # Create and run an applicator.  I wanted to build an interface where you could do
 
70
  # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible.
 
71
  def apply(method, options)
 
72
    raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to]
 
73
    applier = Interface.new(method, options)
 
74
    applier.apply(self)
 
75
  end
 
76
 
 
77
  # If autosign is configured, then autosign all CSRs that match our configuration.
 
78
  def autosign
 
79
    return unless auto = autosign?
 
80
 
 
81
    store = nil
 
82
    store = autosign_store(auto) if auto != true
 
83
 
 
84
    Puppet::SSL::CertificateRequest.indirection.search("*").each do |csr|
 
85
      sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1")
 
86
    end
 
87
  end
 
88
 
 
89
  # Do we autosign?  This returns true, false, or a filename.
 
90
  def autosign?
 
91
    auto = Puppet[:autosign]
 
92
    return false if ['false', false].include?(auto)
 
93
    return true if ['true', true].include?(auto)
 
94
 
 
95
    raise ArgumentError, "The autosign configuration '#{auto}' must be a fully qualified file" unless auto =~ /^\//
 
96
    FileTest.exist?(auto) && auto
 
97
  end
 
98
 
 
99
  # Create an AuthStore for autosigning.
 
100
  def autosign_store(file)
 
101
    auth = Puppet::Network::AuthStore.new
 
102
    File.readlines(file).each do |line|
 
103
      next if line =~ /^\s*#/
 
104
      next if line =~ /^\s*$/
 
105
      auth.allow(line.chomp)
 
106
    end
 
107
 
 
108
    auth
 
109
  end
 
110
 
 
111
  # Retrieve (or create, if necessary) the certificate revocation list.
 
112
  def crl
 
113
    unless defined?(@crl)
 
114
      unless @crl = Puppet::SSL::CertificateRevocationList.indirection.find(Puppet::SSL::CA_NAME)
 
115
        @crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME)
 
116
        @crl.generate(host.certificate.content, host.key.content)
 
117
        Puppet::SSL::CertificateRevocationList.indirection.save(@crl)
 
118
      end
 
119
    end
 
120
    @crl
 
121
  end
 
122
 
 
123
  # Delegate this to our Host class.
 
124
  def destroy(name)
 
125
    Puppet::SSL::Host.destroy(name)
 
126
  end
 
127
 
 
128
  # Generate a new certificate.
 
129
  def generate(name, options = {})
 
130
    raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name)
 
131
    host = Puppet::SSL::Host.new(name)
 
132
 
 
133
    # Pass on any requested subjectAltName field.
 
134
    san = options[:dns_alt_names]
 
135
 
 
136
    host = Puppet::SSL::Host.new(name)
 
137
    host.generate_certificate_request(:dns_alt_names => san)
 
138
    sign(name, !!san)
 
139
  end
 
140
 
 
141
  # Generate our CA certificate.
 
142
  def generate_ca_certificate
 
143
    generate_password unless password?
 
144
 
 
145
    host.generate_key unless host.key
 
146
 
 
147
    # Create a new cert request.  We do this specially, because we don't want
 
148
    # to actually save the request anywhere.
 
149
    request = Puppet::SSL::CertificateRequest.new(host.name)
 
150
 
 
151
    # We deliberately do not put any subjectAltName in here: the CA
 
152
    # certificate absolutely does not need them. --daniel 2011-10-13
 
153
    request.generate(host.key)
 
154
 
 
155
    # Create a self-signed certificate.
 
156
    @certificate = sign(host.name, false, request)
 
157
 
 
158
    # And make sure we initialize our CRL.
 
159
    crl
 
160
  end
 
161
 
 
162
  def initialize
 
163
    Puppet.settings.use :main, :ssl, :ca
 
164
 
 
165
    @name = Puppet[:certname]
 
166
 
 
167
    @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)
 
168
 
 
169
    setup
 
170
  end
 
171
 
 
172
  # Retrieve (or create, if necessary) our inventory manager.
 
173
  def inventory
 
174
    @inventory ||= Puppet::SSL::Inventory.new
 
175
  end
 
176
 
 
177
  # Generate a new password for the CA.
 
178
  def generate_password
 
179
    pass = ""
 
180
    20.times { pass += (rand(74) + 48).chr }
 
181
 
 
182
    begin
 
183
      Puppet.settings.write(:capass) { |f| f.print pass }
 
184
    rescue Errno::EACCES => detail
 
185
      raise Puppet::Error, "Could not write CA password: #{detail}"
 
186
    end
 
187
 
 
188
    @password = pass
 
189
 
 
190
    pass
 
191
  end
 
192
 
 
193
  # List all signed certificates.
 
194
  def list
 
195
    Puppet::SSL::Certificate.indirection.search("*").collect { |c| c.name }
 
196
  end
 
197
 
 
198
  # Read the next serial from the serial file, and increment the
 
199
  # file so this one is considered used.
 
200
  def next_serial
 
201
    serial = nil
 
202
 
 
203
    # This is slightly odd.  If the file doesn't exist, our readwritelock creates
 
204
    # it, but with a mode we can't actually read in some cases.  So, use
 
205
    # a default before the lock.
 
206
    serial = 0x1 unless FileTest.exist?(Puppet[:serial])
 
207
 
 
208
    Puppet.settings.readwritelock(:serial) { |f|
 
209
      serial ||= File.read(Puppet.settings[:serial]).chomp.hex if FileTest.exist?(Puppet[:serial])
 
210
 
 
211
      # We store the next valid serial, not the one we just used.
 
212
      f << "%04X" % (serial + 1)
 
213
    }
 
214
 
 
215
    serial
 
216
  end
 
217
 
 
218
  # Does the password file exist?
 
219
  def password?
 
220
    FileTest.exist? Puppet[:capass]
 
221
  end
 
222
 
 
223
  # Print a given host's certificate as text.
 
224
  def print(name)
 
225
    (cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil
 
226
  end
 
227
 
 
228
  # Revoke a given certificate.
 
229
  def revoke(name)
 
230
    raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl
 
231
 
 
232
    if cert = Puppet::SSL::Certificate.indirection.find(name)
 
233
      serial = cert.content.serial
 
234
    elsif ! serial = inventory.serial(name)
 
235
      raise ArgumentError, "Could not find a serial number for #{name}"
 
236
    end
 
237
    crl.revoke(serial, host.key.content)
 
238
  end
 
239
 
 
240
  # This initializes our CA so it actually works.  This should be a private
 
241
  # method, except that you can't any-instance stub private methods, which is
 
242
  # *awesome*.  This method only really exists to provide a stub-point during
 
243
  # testing.
 
244
  def setup
 
245
    generate_ca_certificate unless @host.certificate
 
246
  end
 
247
 
 
248
  # Sign a given certificate request.
 
249
  def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil)
 
250
    # This is a self-signed certificate
 
251
    if self_signing_csr
 
252
      # # This is a self-signed certificate, which is for the CA.  Since this
 
253
      # # forces the certificate to be self-signed, anyone who manages to trick
 
254
      # # the system into going through this path gets a certificate they could
 
255
      # # generate anyway.  There should be no security risk from that.
 
256
      csr = self_signing_csr
 
257
      cert_type = :ca
 
258
      issuer = csr.content
 
259
    else
 
260
      allow_dns_alt_names = true if hostname == Puppet[:certname].downcase
 
261
      unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname)
 
262
        raise ArgumentError, "Could not find certificate request for #{hostname}"
 
263
      end
 
264
 
 
265
      cert_type = :server
 
266
      issuer = host.certificate.content
 
267
 
 
268
      # Make sure that the CSR conforms to our internal signing policies.
 
269
      # This will raise if the CSR doesn't conform, but just in case...
 
270
      check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or
 
271
        raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!"
 
272
    end
 
273
 
 
274
    cert = Puppet::SSL::Certificate.new(hostname)
 
275
    cert.content = Puppet::SSL::CertificateFactory.
 
276
      build(cert_type, csr, issuer, next_serial)
 
277
    cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new)
 
278
 
 
279
    Puppet.notice "Signed certificate request for #{hostname}"
 
280
 
 
281
    # Add the cert to the inventory before we save it, since
 
282
    # otherwise we could end up with it being duplicated, if
 
283
    # this is the first time we build the inventory file.
 
284
    inventory.add(cert)
 
285
 
 
286
    # Save the now-signed cert.  This should get routed correctly depending
 
287
    # on the certificate type.
 
288
    Puppet::SSL::Certificate.indirection.save(cert)
 
289
 
 
290
    # And remove the CSR if this wasn't self signed.
 
291
    Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr
 
292
 
 
293
    cert
 
294
  end
 
295
 
 
296
  def check_internal_signing_policies(hostname, csr, allow_dns_alt_names)
 
297
    # Reject unknown request extensions.
 
298
    unknown_req = csr.request_extensions.
 
299
      reject {|x| RequestExtensionWhitelist.include? x["oid"] }
 
300
 
 
301
    if unknown_req and not unknown_req.empty?
 
302
      names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ")
 
303
      raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}"
 
304
    end
 
305
 
 
306
    # Wildcards: we don't allow 'em at any point.
 
307
    #
 
308
    # The stringification here makes the content visible, and saves us having
 
309
    # to scrobble through the content of the CSR subject field to make sure it
 
310
    # is what we expect where we expect it.
 
311
    if csr.content.subject.to_s.include? '*'
 
312
      raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}"
 
313
    end
 
314
 
 
315
    unless csr.subject_alt_names.empty?
 
316
      # If you alt names are allowed, they are required. Otherwise they are
 
317
      # disallowed. Self-signed certs are implicitly trusted, however.
 
318
      unless allow_dns_alt_names
 
319
        raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains subject alternative names (#{csr.subject_alt_names.join(', ')}), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{csr.name}` to sign this request."
 
320
      end
 
321
 
 
322
      # If subjectAltNames are present, validate that they are only for DNS
 
323
      # labels, not any other kind.
 
324
      unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ }
 
325
        raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains a subjectAltName outside the DNS label space: #{csr.subject_alt_names.join(', ')}.  To continue, this CSR needs to be cleaned."
 
326
      end
 
327
 
 
328
      # Check for wildcards in the subjectAltName fields too.
 
329
      if csr.subject_alt_names.any? {|x| x.include? '*' }
 
330
        raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' subjectAltName contains a wildcard, which is not allowed: #{csr.subject_alt_names.join(', ')}  To continue, this CSR needs to be cleaned."
 
331
      end
 
332
    end
 
333
 
 
334
    return true                 # good enough for us!
 
335
  end
 
336
 
 
337
  # Verify a given host's certificate.
 
338
  def verify(name)
 
339
    unless cert = Puppet::SSL::Certificate.indirection.find(name)
 
340
      raise ArgumentError, "Could not find a certificate for #{name}"
 
341
    end
 
342
    store = OpenSSL::X509::Store.new
 
343
    store.add_file Puppet[:cacert]
 
344
    store.add_crl crl.content if self.crl
 
345
    store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
 
346
    store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK if Puppet.settings[:certificate_revocation]
 
347
 
 
348
    raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content)
 
349
  end
 
350
 
 
351
  def fingerprint(name, md = :MD5)
 
352
    unless cert = Puppet::SSL::Certificate.indirection.find(name) || Puppet::SSL::CertificateRequest.indirection.find(name)
 
353
      raise ArgumentError, "Could not find a certificate or csr for #{name}"
 
354
    end
 
355
    cert.fingerprint(md)
 
356
  end
 
357
 
 
358
  # List the waiting certificate requests.
 
359
  def waiting?
 
360
    Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name }
 
361
  end
 
362
end