puppet 2.7.5 → 2.7.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

Files changed (140) hide show
  1. data/CHANGELOG +121 -0
  2. data/conf/redhat/puppet.spec +16 -7
  3. data/lib/puppet.rb +1 -1
  4. data/lib/puppet/application/cert.rb +17 -3
  5. data/lib/puppet/application/device.rb +1 -0
  6. data/lib/puppet/application/kick.rb +0 -2
  7. data/lib/puppet/application/resource.rb +73 -66
  8. data/lib/puppet/configurer/plugin_handler.rb +6 -2
  9. data/lib/puppet/defaults.rb +60 -5
  10. data/lib/puppet/face/ca.rb +11 -2
  11. data/lib/puppet/face/certificate.rb +33 -4
  12. data/lib/puppet/file_serving/fileset.rb +1 -1
  13. data/lib/puppet/file_serving/indirection_hooks.rb +2 -2
  14. data/lib/puppet/file_serving/metadata.rb +43 -4
  15. data/lib/puppet/indirector.rb +0 -1
  16. data/lib/puppet/indirector/request.rb +3 -4
  17. data/lib/puppet/indirector/resource/active_record.rb +3 -10
  18. data/lib/puppet/indirector/resource/ral.rb +2 -2
  19. data/lib/puppet/indirector/rest.rb +1 -1
  20. data/lib/puppet/network/handler/ca.rb +16 -106
  21. data/lib/puppet/network/handler/master.rb +0 -3
  22. data/lib/puppet/network/handler/runner.rb +1 -0
  23. data/lib/puppet/parser/scope.rb +10 -0
  24. data/lib/puppet/provider/file/posix.rb +72 -34
  25. data/lib/puppet/provider/file/windows.rb +100 -0
  26. data/lib/puppet/provider/group/windows_adsi.rb +2 -2
  27. data/lib/puppet/provider/user/windows_adsi.rb +19 -4
  28. data/lib/puppet/resource.rb +16 -0
  29. data/lib/puppet/resource/catalog.rb +1 -1
  30. data/lib/puppet/ssl/certificate.rb +2 -2
  31. data/lib/puppet/ssl/certificate_authority.rb +86 -10
  32. data/lib/puppet/ssl/certificate_authority/interface.rb +64 -19
  33. data/lib/puppet/ssl/certificate_factory.rb +112 -91
  34. data/lib/puppet/ssl/certificate_request.rb +88 -1
  35. data/lib/puppet/ssl/host.rb +20 -3
  36. data/lib/puppet/type/file.rb +15 -34
  37. data/lib/puppet/type/file/group.rb +11 -91
  38. data/lib/puppet/type/file/mode.rb +11 -41
  39. data/lib/puppet/type/file/owner.rb +18 -34
  40. data/lib/puppet/type/file/source.rb +22 -7
  41. data/lib/puppet/type/group.rb +4 -3
  42. data/lib/puppet/type/user.rb +4 -1
  43. data/lib/puppet/util.rb +59 -6
  44. data/lib/puppet/util/adsi.rb +11 -0
  45. data/lib/puppet/util/log.rb +4 -0
  46. data/lib/puppet/util/log/destinations.rb +7 -1
  47. data/lib/puppet/util/monkey_patches.rb +19 -0
  48. data/lib/puppet/util/network_device/config.rb +4 -5
  49. data/lib/puppet/util/settings.rb +5 -0
  50. data/lib/puppet/util/suidmanager.rb +0 -1
  51. data/lib/puppet/util/windows.rb +4 -0
  52. data/lib/puppet/util/windows/error.rb +16 -0
  53. data/lib/puppet/util/windows/security.rb +593 -0
  54. data/spec/integration/defaults_spec.rb +27 -0
  55. data/spec/integration/network/handler_spec.rb +1 -1
  56. data/spec/integration/type/file_spec.rb +382 -145
  57. data/spec/integration/util/windows/security_spec.rb +468 -0
  58. data/spec/shared_behaviours/file_serving.rb +4 -3
  59. data/spec/unit/application/agent_spec.rb +1 -0
  60. data/spec/unit/application/device_spec.rb +5 -0
  61. data/spec/unit/application/resource_spec.rb +62 -101
  62. data/spec/unit/configurer/downloader_spec.rb +2 -2
  63. data/spec/unit/configurer/plugin_handler_spec.rb +15 -8
  64. data/spec/unit/configurer_spec.rb +2 -2
  65. data/spec/unit/face/ca_spec.rb +34 -0
  66. data/spec/unit/face/certificate_spec.rb +168 -1
  67. data/spec/unit/file_serving/fileset_spec.rb +1 -1
  68. data/spec/unit/file_serving/indirection_hooks_spec.rb +1 -1
  69. data/spec/unit/file_serving/metadata_spec.rb +151 -107
  70. data/spec/unit/indirector/certificate_request/ca_spec.rb +0 -3
  71. data/spec/unit/indirector/direct_file_server_spec.rb +10 -9
  72. data/spec/unit/indirector/file_metadata/file_spec.rb +6 -4
  73. data/spec/unit/indirector/request_spec.rb +13 -3
  74. data/spec/unit/indirector/resource/active_record_spec.rb +4 -10
  75. data/spec/unit/indirector/resource/ral_spec.rb +6 -4
  76. data/spec/unit/indirector/rest_spec.rb +5 -6
  77. data/spec/unit/network/handler/ca_spec.rb +86 -0
  78. data/spec/unit/parser/collector_spec.rb +7 -7
  79. data/spec/unit/parser/scope_spec.rb +20 -0
  80. data/spec/unit/provider/file/posix_spec.rb +226 -0
  81. data/spec/unit/provider/file/windows_spec.rb +136 -0
  82. data/spec/unit/provider/group/windows_adsi_spec.rb +7 -2
  83. data/spec/unit/provider/user/windows_adsi_spec.rb +36 -3
  84. data/spec/unit/resource/catalog_spec.rb +20 -10
  85. data/spec/unit/resource_spec.rb +55 -8
  86. data/spec/unit/ssl/certificate_authority/interface_spec.rb +97 -54
  87. data/spec/unit/ssl/certificate_authority_spec.rb +133 -23
  88. data/spec/unit/ssl/certificate_factory_spec.rb +90 -70
  89. data/spec/unit/ssl/certificate_request_spec.rb +62 -1
  90. data/spec/unit/ssl/certificate_spec.rb +20 -14
  91. data/spec/unit/ssl/host_spec.rb +52 -6
  92. data/spec/unit/type/file/content_spec.rb +4 -4
  93. data/spec/unit/type/file/group_spec.rb +34 -96
  94. data/spec/unit/type/file/mode_spec.rb +88 -0
  95. data/spec/unit/type/file/owner_spec.rb +32 -123
  96. data/spec/unit/type/file/source_spec.rb +120 -41
  97. data/spec/unit/type/file_spec.rb +1033 -753
  98. data/spec/unit/type_spec.rb +19 -1
  99. data/spec/unit/util/adsi_spec.rb +19 -0
  100. data/spec/unit/util/log/destinations_spec.rb +75 -0
  101. data/spec/unit/util/log_spec.rb +15 -0
  102. data/spec/unit/util/network_device/config_spec.rb +7 -0
  103. data/spec/unit/util/settings_spec.rb +10 -0
  104. data/spec/unit/util_spec.rb +126 -13
  105. data/test/language/functions.rb +0 -1
  106. data/test/language/snippets.rb +0 -9
  107. data/test/lib/puppettest/exetest.rb +1 -1
  108. data/test/lib/puppettest/servertest.rb +0 -1
  109. data/test/rails/rails.rb +0 -1
  110. data/test/ral/type/filesources.rb +0 -60
  111. metadata +13 -33
  112. data/lib/puppet/network/client.rb +0 -174
  113. data/lib/puppet/network/client/ca.rb +0 -56
  114. data/lib/puppet/network/client/file.rb +0 -6
  115. data/lib/puppet/network/client/proxy.rb +0 -27
  116. data/lib/puppet/network/client/report.rb +0 -26
  117. data/lib/puppet/network/client/runner.rb +0 -10
  118. data/lib/puppet/network/client/status.rb +0 -4
  119. data/lib/puppet/network/http_server.rb +0 -3
  120. data/lib/puppet/network/http_server/mongrel.rb +0 -130
  121. data/lib/puppet/network/http_server/webrick.rb +0 -155
  122. data/lib/puppet/network/xmlrpc/client.rb +0 -211
  123. data/lib/puppet/provider/file/win32.rb +0 -72
  124. data/lib/puppet/sslcertificates.rb +0 -146
  125. data/lib/puppet/sslcertificates/ca.rb +0 -375
  126. data/lib/puppet/sslcertificates/certificate.rb +0 -255
  127. data/lib/puppet/sslcertificates/inventory.rb +0 -38
  128. data/lib/puppet/sslcertificates/support.rb +0 -146
  129. data/spec/integration/network/client_spec.rb +0 -18
  130. data/spec/unit/network/xmlrpc/client_spec.rb +0 -172
  131. data/spec/unit/sslcertificates/ca_spec.rb +0 -106
  132. data/test/certmgr/certmgr.rb +0 -308
  133. data/test/certmgr/inventory.rb +0 -69
  134. data/test/certmgr/support.rb +0 -105
  135. data/test/network/client/ca.rb +0 -69
  136. data/test/network/client/dipper.rb +0 -34
  137. data/test/network/handler/ca.rb +0 -273
  138. data/test/network/server/mongrel_test.rb +0 -99
  139. data/test/network/server/webrick.rb +0 -111
  140. data/test/network/xmlrpc/client.rb +0 -45
@@ -9,7 +9,7 @@ module Puppet
9
9
 
10
10
  class InterfaceError < ArgumentError; end
11
11
 
12
- attr_reader :method, :subjects, :digest
12
+ attr_reader :method, :subjects, :digest, :options
13
13
 
14
14
  # Actually perform the work.
15
15
  def apply(ca)
@@ -35,49 +35,94 @@ module Puppet
35
35
  raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all
36
36
 
37
37
  subjects.each do |host|
38
- ca.generate(host)
38
+ ca.generate(host, options)
39
39
  end
40
40
  end
41
41
 
42
42
  def initialize(method, options)
43
43
  self.method = method
44
- self.subjects = options[:to]
45
- @digest = options[:digest] || :MD5
44
+ self.subjects = options.delete(:to)
45
+ @digest = options.delete(:digest) || :MD5
46
+ @options = options
46
47
  end
47
48
 
48
49
  # List the hosts.
49
50
  def list(ca)
50
- unless subjects
51
- puts ca.waiting?.join("\n")
52
- return nil
53
- end
54
-
55
51
  signed = ca.list
56
52
  requests = ca.waiting?
57
53
 
58
- if subjects == :all
54
+ case subjects
55
+ when :all
59
56
  hosts = [signed, requests].flatten
60
- elsif subjects == :signed
57
+ when :signed
61
58
  hosts = signed.flatten
59
+ when nil
60
+ hosts = requests
62
61
  else
63
62
  hosts = subjects
64
63
  end
65
64
 
65
+ certs = {:signed => {}, :invalid => {}, :request => {}}
66
+
67
+ return if hosts.empty?
68
+
66
69
  hosts.uniq.sort.each do |host|
67
- invalid = false
68
70
  begin
69
71
  ca.verify(host) unless requests.include?(host)
70
72
  rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => details
71
- invalid = details.to_s
73
+ verify_error = details.to_s
72
74
  end
73
- if not invalid and signed.include?(host)
74
- puts "+ #{host} (#{ca.fingerprint(host, @digest)})"
75
- elsif invalid
76
- puts "- #{host} (#{ca.fingerprint(host, @digest)}) (#{invalid})"
75
+
76
+ if verify_error
77
+ cert = Puppet::SSL::Certificate.indirection.find(host)
78
+ certs[:invalid][host] = [cert, verify_error]
79
+ elsif signed.include?(host)
80
+ cert = Puppet::SSL::Certificate.indirection.find(host)
81
+ certs[:signed][host] = cert
77
82
  else
78
- puts "#{host} (#{ca.fingerprint(host, @digest)})"
83
+ req = Puppet::SSL::CertificateRequest.indirection.find(host)
84
+ certs[:request][host] = req
79
85
  end
80
86
  end
87
+
88
+ names = certs.values.map(&:keys).flatten
89
+
90
+ name_width = names.sort_by(&:length).last.length rescue 0
91
+
92
+ output = [:request, :signed, :invalid].map do |type|
93
+ next if certs[type].empty?
94
+
95
+ certs[type].map do |host,info|
96
+ format_host(ca, host, type, info, name_width)
97
+ end
98
+ end.flatten.compact.sort.join("\n")
99
+
100
+ puts output
101
+ end
102
+
103
+ def format_host(ca, host, type, info, width)
104
+ certish, verify_error = info
105
+ alt_names = case type
106
+ when :signed
107
+ certish.subject_alt_names
108
+ when :request
109
+ certish.subject_alt_names
110
+ else
111
+ []
112
+ end
113
+
114
+ alt_names.delete(host)
115
+
116
+ alt_str = "(alt names: #{alt_names.join(', ')})" unless alt_names.empty?
117
+
118
+ glyph = {:signed => '+', :request => ' ', :invalid => '-'}[type]
119
+
120
+ name = host.ljust(width)
121
+ fingerprint = "(#{ca.fingerprint(host, @digest)})"
122
+
123
+ explanation = "(#{verify_error})" if verify_error
124
+
125
+ [glyph, name, fingerprint, alt_str, explanation].compact.join(' ')
81
126
  end
82
127
 
83
128
  # Set the method to apply.
@@ -113,7 +158,7 @@ module Puppet
113
158
  list = subjects == :all ? ca.waiting? : subjects
114
159
  raise InterfaceError, "No waiting certificate requests to sign" if list.empty?
115
160
  list.each do |host|
116
- ca.sign(host)
161
+ ca.sign(host, options[:allow_dns_alt_names])
117
162
  end
118
163
  end
119
164
 
@@ -2,7 +2,7 @@ require 'puppet/ssl'
2
2
 
3
3
  # The tedious class that does all the manipulations to the
4
4
  # certificate to correctly sign it. Yay.
5
- class Puppet::SSL::CertificateFactory
5
+ module Puppet::SSL::CertificateFactory
6
6
  # How we convert from various units to the required seconds.
7
7
  UNITMAP = {
8
8
  "y" => 365 * 24 * 60 * 60,
@@ -11,75 +11,84 @@ class Puppet::SSL::CertificateFactory
11
11
  "s" => 1
12
12
  }
13
13
 
14
- attr_reader :name, :cert_type, :csr, :issuer, :serial
14
+ def self.build(cert_type, csr, issuer, serial)
15
+ # Work out if we can even build the requested type of certificate.
16
+ build_extensions = "build_#{cert_type.to_s}_extensions"
17
+ respond_to?(build_extensions) or
18
+ raise ArgumentError, "#{cert_type.to_s} is an invalid certificate type!"
15
19
 
16
- def initialize(cert_type, csr, issuer, serial)
17
- @cert_type, @csr, @issuer, @serial = cert_type, csr, issuer, serial
20
+ # set up the certificate, and start building the content.
21
+ cert = OpenSSL::X509::Certificate.new
18
22
 
19
- @name = @csr.subject
20
- end
21
-
22
- # Actually generate our certificate.
23
- def result
24
- @cert = OpenSSL::X509::Certificate.new
23
+ cert.version = 2 # X509v3
24
+ cert.subject = csr.content.subject
25
+ cert.issuer = issuer.subject
26
+ cert.public_key = csr.content.public_key
27
+ cert.serial = serial
25
28
 
26
- @cert.version = 2 # X509v3
27
- @cert.subject = @csr.subject
28
- @cert.issuer = @issuer.subject
29
- @cert.public_key = @csr.public_key
30
- @cert.serial = @serial
29
+ # Make the certificate valid as of yesterday, because so many people's
30
+ # clocks are out of sync. This gives one more day of validity than people
31
+ # might expect, but is better than making every person who has a messed up
32
+ # clock fail, and better than having every cert we generate expire a day
33
+ # before the user expected it to when they asked for "one year".
34
+ cert.not_before = Time.now - (60*60*24)
35
+ cert.not_after = Time.now + ttl
31
36
 
32
- build_extensions
37
+ add_extensions_to(cert, csr, issuer, send(build_extensions))
33
38
 
34
- set_ttl
35
-
36
- @cert
39
+ return cert
37
40
  end
38
41
 
39
42
  private
40
43
 
41
- # This is pretty ugly, but I'm not really sure it's even possible to do
42
- # it any other way.
43
- def build_extensions
44
- @ef = OpenSSL::X509::ExtensionFactory.new
45
-
46
- @ef.subject_certificate = @cert
44
+ def self.add_extensions_to(cert, csr, issuer, extensions)
45
+ ef = OpenSSL::X509::ExtensionFactory.
46
+ new(cert, issuer.is_a?(OpenSSL::X509::Request) ? cert : issuer)
47
47
 
48
- if @issuer.is_a?(OpenSSL::X509::Request) # It's a self-signed cert
49
- @ef.issuer_certificate = @cert
50
- else
51
- @ef.issuer_certificate = @issuer
48
+ # Extract the requested extensions from the CSR.
49
+ requested_exts = csr.request_extensions.inject({}) do |hash, re|
50
+ hash[re["oid"]] = [re["value"], re["critical"]]
51
+ hash
52
52
  end
53
53
 
54
- @subject_alt_name = []
55
- @key_usage = nil
56
- @ext_key_usage = nil
57
- @extensions = []
58
-
59
- method = "add_#{@cert_type.to_s}_extensions"
60
-
61
- begin
62
- send(method)
63
- rescue NoMethodError
64
- raise ArgumentError, "#{@cert_type} is an invalid certificate type"
54
+ # Produce our final set of extensions. We deliberately order these to
55
+ # build the way we want:
56
+ # 1. "safe" default values, like the comment, that no one cares about.
57
+ # 2. request extensions, from the CSR
58
+ # 3. extensions based on the type we are generating
59
+ # 4. overrides, which we always want to have in their form
60
+ #
61
+ # This ordering *is* security-critical, but we want to allow the user
62
+ # enough rope to shoot themselves in the foot, if they want to ignore our
63
+ # advice and externally approve a CSR that sets the basicConstraints.
64
+ #
65
+ # Swapping the order of 2 and 3 would ensure that you couldn't slip a
66
+ # certificate through where the CA constraint was true, though, if
67
+ # something went wrong up there. --daniel 2011-10-11
68
+ defaults = { "nsComment" => "Puppet Ruby/OpenSSL Internal Certificate" }
69
+ override = { "subjectKeyIdentifier" => "hash" }
70
+
71
+ exts = [defaults, requested_exts, extensions, override].
72
+ inject({}) {|ret, val| ret.merge(val) }
73
+
74
+ cert.extensions = exts.map do |oid, val|
75
+ val, crit = *val
76
+ val = val.join(', ') unless val.is_a? String
77
+
78
+ # Enforce the X509v3 rules about subjectAltName being critical:
79
+ # specifically, it SHOULD NOT be critical if we have a subject, which we
80
+ # always do. --daniel 2011-10-18
81
+ crit = false if oid == "subjectAltName"
82
+
83
+ # val can be either a string, or [string, critical], and this does the
84
+ # right thing regardless of what we get passed.
85
+ ef.create_ext(oid, val, crit)
65
86
  end
66
-
67
- @extensions << @ef.create_extension("nsComment", "Puppet Ruby/OpenSSL Generated Certificate")
68
- @extensions << @ef.create_extension("basicConstraints", @basic_constraint, true)
69
- @extensions << @ef.create_extension("subjectKeyIdentifier", "hash")
70
- @extensions << @ef.create_extension("keyUsage", @key_usage.join(",")) if @key_usage
71
- @extensions << @ef.create_extension("extendedKeyUsage", @ext_key_usage.join(",")) if @ext_key_usage
72
- @extensions << @ef.create_extension("subjectAltName", @subject_alt_name.join(",")) if ! @subject_alt_name.empty?
73
-
74
- @cert.extensions = @extensions
75
-
76
- # for some reason this _must_ be the last extension added
77
- @extensions << @ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") if @cert_type == :ca
78
87
  end
79
88
 
80
89
  # TTL for new certificates in seconds. If config param :ca_ttl is set,
81
90
  # use that, otherwise use :ca_days for backwards compatibility
82
- def ttl
91
+ def self.ttl
83
92
  ttl = Puppet.settings[:ca_ttl]
84
93
 
85
94
  return ttl unless ttl.is_a?(String)
@@ -89,57 +98,69 @@ class Puppet::SSL::CertificateFactory
89
98
  $1.to_i * UNITMAP[$2]
90
99
  end
91
100
 
92
- def set_ttl
93
- # Make the certificate valid as of yesterday, because
94
- # so many people's clocks are out of sync.
95
- from = Time.now - (60*60*24)
96
- @cert.not_before = from
97
- @cert.not_after = from + ttl
98
- end
99
-
100
101
  # Woot! We're a CA.
101
- def add_ca_extensions
102
- @basic_constraint = "CA:TRUE"
103
- @key_usage = %w{cRLSign keyCertSign}
102
+ def self.build_ca_extensions
103
+ {
104
+ # This was accidentally omitted in the previous version of this code: an
105
+ # effort was made to add it last, but that actually managed to avoid
106
+ # adding it to the certificate at all.
107
+ #
108
+ # We have some sort of bug, which means that when we add it we get a
109
+ # complaint that the issuer keyid can't be fetched, which breaks all
110
+ # sorts of things in our test suite and, e.g., bootstrapping the CA.
111
+ #
112
+ # http://tools.ietf.org/html/rfc5280#section-4.2.1.1 says that, to be a
113
+ # conforming CA we MAY omit the field if we are self-signed, which I
114
+ # think gives us a pass in the specific case.
115
+ #
116
+ # It also notes that we MAY derive the ID from the subject and serial
117
+ # number of the issuer, or from the key ID, and we definitely have the
118
+ # former data, should we want to restore this...
119
+ #
120
+ # Anyway, preserving this bug means we don't risk breaking anything in
121
+ # the field, even though it would be nice to have. --daniel 2011-10-11
122
+ #
123
+ # "authorityKeyIdentifier" => "keyid:always,issuer:always",
124
+ "keyUsage" => [%w{cRLSign keyCertSign}, true],
125
+ "basicConstraints" => ["CA:TRUE", true],
126
+ }
104
127
  end
105
128
 
106
129
  # We're a terminal CA, probably not self-signed.
107
- def add_terminalsubca_extensions
108
- @basic_constraint = "CA:TRUE,pathlen:0"
109
- @key_usage = %w{cRLSign keyCertSign}
130
+ def self.build_terminalsubca_extensions
131
+ {
132
+ "keyUsage" => [%w{cRLSign keyCertSign}, true],
133
+ "basicConstraints" => ["CA:TRUE,pathlen:0", true],
134
+ }
110
135
  end
111
136
 
112
137
  # We're a normal server.
113
- def add_server_extensions
114
- @basic_constraint = "CA:FALSE"
115
- dnsnames = Puppet[:certdnsnames]
116
- name = @name.to_s.sub(%r{/CN=},'')
117
- if dnsnames != ""
118
- dnsnames.split(':').each { |d| @subject_alt_name << 'DNS:' + d }
119
- @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias
120
- elsif name == Facter.value(:fqdn) # we're a CA server, and thus probably the server
121
- @subject_alt_name << 'DNS:' + "puppet" # Add 'puppet' as an alias
122
- @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias
123
- @subject_alt_name << 'DNS:' + name.sub(/^[^.]+./, "puppet.") # add puppet.domain as an alias
124
- end
125
- @key_usage = %w{digitalSignature keyEncipherment}
126
- @ext_key_usage = %w{serverAuth clientAuth emailProtection}
138
+ def self.build_server_extensions
139
+ {
140
+ "keyUsage" => [%w{digitalSignature keyEncipherment}, true],
141
+ "extendedKeyUsage" => [%w{serverAuth clientAuth}, true],
142
+ "basicConstraints" => ["CA:FALSE", true],
143
+ }
127
144
  end
128
145
 
129
146
  # Um, no idea.
130
- def add_ocsp_extensions
131
- @basic_constraint = "CA:FALSE"
132
- @key_usage = %w{nonRepudiation digitalSignature}
133
- @ext_key_usage = %w{serverAuth OCSPSigning}
147
+ def self.build_ocsp_extensions
148
+ {
149
+ "keyUsage" => [%w{nonRepudiation digitalSignature}, true],
150
+ "extendedKeyUsage" => [%w{serverAuth OCSPSigning}, true],
151
+ "basicConstraints" => ["CA:FALSE", true],
152
+ }
134
153
  end
135
154
 
136
155
  # Normal client.
137
- def add_client_extensions
138
- @basic_constraint = "CA:FALSE"
139
- @key_usage = %w{nonRepudiation digitalSignature keyEncipherment}
140
- @ext_key_usage = %w{clientAuth emailProtection}
141
-
142
- @extensions << @ef.create_extension("nsCertType", "client,email")
156
+ def self.build_client_extensions
157
+ {
158
+ "keyUsage" => [%w{nonRepudiation digitalSignature keyEncipherment}, true],
159
+ # We don't seem to use this, but that seems much more reasonable here...
160
+ "extendedKeyUsage" => [%w{clientAuth emailProtection}, true],
161
+ "basicConstraints" => ["CA:FALSE", true],
162
+ "nsCertType" => "client,email",
163
+ }
143
164
  end
144
165
  end
145
166
 
@@ -35,8 +35,12 @@ class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
35
35
  [:s]
36
36
  end
37
37
 
38
+ def extension_factory
39
+ @ef ||= OpenSSL::X509::ExtensionFactory.new
40
+ end
41
+
38
42
  # How to create a certificate request with our system defaults.
39
- def generate(key)
43
+ def generate(key, options = {})
40
44
  Puppet.info "Creating a new SSL certificate request for #{name}"
41
45
 
42
46
  # Support either an actual SSL key, or a Puppet key.
@@ -51,6 +55,19 @@ class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
51
55
  csr.version = 0
52
56
  csr.subject = OpenSSL::X509::Name.new([["CN", common_name]])
53
57
  csr.public_key = key.public_key
58
+
59
+ if options[:dns_alt_names] then
60
+ names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name]
61
+ names = names.sort.uniq.map {|name| "DNS:#{name}" }.join(", ")
62
+ names = extension_factory.create_extension("subjectAltName", names, false)
63
+
64
+ extReq = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([names])])
65
+
66
+ # We only support the standard request extensions. If you really need
67
+ # msExtReq support, let us know and we can restore them. --daniel 2011-10-10
68
+ csr.add_attribute(OpenSSL::X509::Attribute.new("extReq", extReq))
69
+ end
70
+
54
71
  csr.sign(key, OpenSSL::Digest::MD5.new)
55
72
 
56
73
  raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for #{name} on the server" unless csr.verify(key.public_key)
@@ -59,4 +76,74 @@ class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
59
76
  Puppet.info "Certificate Request fingerprint (md5): #{fingerprint}"
60
77
  @content
61
78
  end
79
+
80
+ # Return the set of extensions requested on this CSR, in a form designed to
81
+ # be useful to Ruby: a hash. Which, not coincidentally, you can pass
82
+ # successfully to the OpenSSL constructor later, if you want.
83
+ def request_extensions
84
+ raise Puppet::Error, "CSR needs content to extract fields" unless @content
85
+
86
+ # Prefer the standard extReq, but accept the Microsoft specific version as
87
+ # a fallback, if the standard version isn't found.
88
+ ext = @content.attributes.find {|x| x.oid == "extReq" } or
89
+ @content.attributes.find {|x| x.oid == "msExtReq" }
90
+ return [] unless ext
91
+
92
+ # Assert the structure and extract the names into an array of arrays.
93
+ unless ext.value.is_a? OpenSSL::ASN1::Set
94
+ raise Puppet::Error, "In #{ext.oid}, expected Set but found #{ext.value.class}"
95
+ end
96
+
97
+ unless ext.value.value.is_a? Array
98
+ raise Puppet::Error, "In #{ext.oid}, expected Set[Array] but found #{ext.value.value.class}"
99
+ end
100
+
101
+ unless ext.value.value.length == 1
102
+ raise Puppet::Error, "In #{ext.oid}, expected Set[Array[...]], but found #{ext.value.value.length} items in the array"
103
+ end
104
+
105
+ san = ext.value.value.first
106
+ unless san.is_a? OpenSSL::ASN1::Sequence
107
+ raise Puppet::Error, "In #{ext.oid}, expected Set[Array[Sequence[...]]], but found #{san.class}"
108
+ end
109
+ san = san.value
110
+
111
+ # OK, now san should be the array of items, validate that...
112
+ index = -1
113
+ san.map do |name|
114
+ index += 1
115
+
116
+ unless name.is_a? OpenSSL::ASN1::Sequence
117
+ raise Puppet::Error, "In #{ext.oid}, expected request extension record #{index} to be a Sequence, but found #{name.class}"
118
+ end
119
+ name = name.value
120
+
121
+ # OK, turn that into an extension, to unpack the content. Lovely that
122
+ # we have to swap the order of arguments to the underlying method, or
123
+ # perhaps that the ASN.1 representation chose to pack them in a
124
+ # strange order where the optional component comes *earlier* than the
125
+ # fixed component in the sequence.
126
+ case name.length
127
+ when 2
128
+ ev = OpenSSL::X509::Extension.new(name[0].value, name[1].value)
129
+ { "oid" => ev.oid, "value" => ev.value }
130
+
131
+ when 3
132
+ ev = OpenSSL::X509::Extension.new(name[0].value, name[2].value, name[1].value)
133
+ { "oid" => ev.oid, "value" => ev.value, "critical" => ev.critical? }
134
+
135
+ else
136
+ raise Puppet::Error, "In #{ext.oid}, expected extension record #{index} to have two or three items, but found #{name.length}"
137
+ end
138
+ end.flatten
139
+ end
140
+
141
+ def subject_alt_names
142
+ @subject_alt_names ||= request_extensions.
143
+ select {|x| x["oid"] = "subjectAltName" }.
144
+ map {|x| x["value"].split(/\s*,\s*/) }.
145
+ flatten.
146
+ sort.
147
+ uniq
148
+ end
62
149
  end