class Puppet::SSL::CertificateRequest

This class creates and manages X509 certificate signing requests.

## CSR attributes

CSRs may contain a set of attributes that includes supplementary information about the CSR or information for the signed certificate.

PKCS#9/RFC 2985 section 5.4 formally defines the “Challenge password”, “Extension request”, and “Extended-certificate attributes”, but this implementation only handles the “Extension request” attribute. Other attributes may be defined on a CSR, but the RFC doesn't define behavior for any other attributes so we treat them as only informational.

## CSR Extension request attribute

CSRs may contain an optional set of extension requests, which allow CSRs to include additional information that may be included in the signed certificate. Any additional information that should be copied from the CSR to the signed certificate MUST be included in this attribute.

This behavior is dictated by PKCS#9/RFC 2985 section 5.4.2.

@see tools.ietf.org/html/rfc2985 “RFC 2985 Section 5.4.2 Extension request”

Constants

PRIVATE_CSR_ATTRIBUTES

Exclude OIDs that may conflict with how Puppet creates CSRs.

We only have nominal support for Microsoft extension requests, but since we ultimately respect that field when looking for extension requests in a CSR we need to prevent that field from being written to directly.

PRIVATE_EXTENSIONS

Public Class Methods

supported_formats() click to toggle source

Because of how the format handler class is included, this can't be in the base class.

   # File lib/puppet/ssl/certificate_request.rb
33 def self.supported_formats
34   [:s]
35 end

Public Instance Methods

custom_attributes() click to toggle source

Return all user specified attributes attached to this CSR as a hash. IF an OID has a single value it is returned as a string, otherwise all values are returned as an array.

The format of CSR attributes is specified in PKCS#10/RFC 2986

@see tools.ietf.org/html/rfc2986 “RFC 2986 Certification Request Syntax Specification”

@api public

@return [Hash<String, String>]

    # File lib/puppet/ssl/certificate_request.rb
192 def custom_attributes
193   x509_attributes = @content.attributes.reject do |attr|
194     PRIVATE_CSR_ATTRIBUTES.include? attr.oid
195   end
196 
197   x509_attributes.map do |attr|
198     {"oid" => attr.oid, "value" => attr.value.value.first.value}
199   end
200 end
ext_value_to_ruby_value(asn1_arr) click to toggle source
    # File lib/puppet/ssl/certificate_request.rb
101 def ext_value_to_ruby_value(asn1_arr)
102   # A list of ASN1 types than can't be directly converted to a Ruby type
103   @non_convertible ||= [OpenSSL::ASN1::EndOfContent,
104                         OpenSSL::ASN1::BitString,
105                         OpenSSL::ASN1::Null,
106                         OpenSSL::ASN1::Enumerated,
107                         OpenSSL::ASN1::UTCTime,
108                         OpenSSL::ASN1::GeneralizedTime,
109                         OpenSSL::ASN1::Sequence,
110                         OpenSSL::ASN1::Set]
111 
112   begin
113     # Attempt to decode the extension's DER data located in the original OctetString
114     asn1_val = OpenSSL::ASN1.decode(asn1_arr.last.value)
115   rescue OpenSSL::ASN1::ASN1Error
116     # This is to allow supporting the old-style of not DER encoding trusted facts
117     return asn1_arr.last.value
118   end
119 
120   # If the extension value can not be directly converted to an atomic Ruby
121   # type, use the original ASN1 value. This is needed to work around a bug
122   # in Ruby's OpenSSL library which doesn't convert the value of unknown
123   # extension OIDs properly. See PUP-3560
124   if @non_convertible.include?(asn1_val.class) then
125     # Allows OpenSSL to take the ASN1 value and turn it into something Ruby understands
126     OpenSSL::X509::Extension.new(asn1_arr.first.value, asn1_val.to_der).value
127   else
128     asn1_val.value
129   end
130 end
extension_factory() click to toggle source
   # File lib/puppet/ssl/certificate_request.rb
37 def extension_factory
38   @ef ||= OpenSSL::X509::ExtensionFactory.new
39 end
generate(key, options = {}) click to toggle source

Create a certificate request with our system settings.

@param key [OpenSSL::X509::Key] The private key associated with this CSR. @param options [Hash] @option options [String] :dns_alt_names A comma separated list of

Subject Alternative Names to include in the CSR extension request.

@option options [Hash<String, String, Array<String>>] :csr_attributes A hash

of OIDs and values that are either a string or array of strings.

@option options [Array<String, String>] :extension_requests A hash of

certificate extensions to add to the CSR extReq attribute, excluding
the Subject Alternative Names extension.

@raise [Puppet::Error] If the generated CSR signature couldn't be verified

@return [OpenSSL::X509::Request] The generated CSR

   # File lib/puppet/ssl/certificate_request.rb
56 def generate(key, options = {})
57   Puppet.info _("Creating a new SSL certificate request for %{name}") % { name: name }
58 
59   # If we're a CSR for the CA, then use the real ca_name, rather than the
60   # fake 'ca' name.  This is mostly for backward compatibility with 0.24.x,
61   # but it's also just a good idea.
62   common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name
63 
64   csr = OpenSSL::X509::Request.new
65   csr.version = 0
66   csr.subject = OpenSSL::X509::Name.new([["CN", common_name]])
67 
68   csr.public_key = if key.is_a?(OpenSSL::PKey::EC)
69                      # EC#public_key doesn't follow the PKey API,
70                      # see https://github.com/ruby/openssl/issues/29
71                      point = key.public_key
72                      pubkey = OpenSSL::PKey::EC.new(point.group)
73                      pubkey.public_key = point
74                      pubkey
75                    else
76                      key.public_key
77                    end
78 
79   if options[:csr_attributes]
80     add_csr_attributes(csr, options[:csr_attributes])
81   end
82 
83   if (ext_req_attribute = extension_request_attribute(options))
84     csr.add_attribute(ext_req_attribute)
85   end
86 
87   signer = Puppet::SSL::CertificateSigner.new
88   signer.sign(csr, key)
89 
90   raise Puppet::Error, _("CSR sign verification failed; you need to clean the certificate request for %{name} on the server") % { name: name } unless csr.verify(csr.public_key)
91 
92   @content = csr
93 
94   # we won't be able to get the digest on jruby
95   if @content.signature_algorithm
96     Puppet.info _("Certificate Request fingerprint (%{digest}): %{hex_digest}") % { digest: digest.name, hex_digest: digest.to_hex }
97   end
98   @content
99 end
request_extensions() click to toggle source

Return the set of extensions requested on this CSR, in a form designed to be useful to Ruby: an array of hashes. Which, not coincidentally, you can pass successfully to the OpenSSL constructor later, if you want.

@return [Array<Hash{String => String}>] An array of two or three element hashes, with key/value pairs for the extension's oid, its value, and optionally its critical state.

    # File lib/puppet/ssl/certificate_request.rb
139 def request_extensions
140   raise Puppet::Error, _("CSR needs content to extract fields") unless @content
141 
142   # Prefer the standard extReq, but accept the Microsoft specific version as
143   # a fallback, if the standard version isn't found.
144   attribute   = @content.attributes.find {|x| x.oid == "extReq" }
145   attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" }
146   return [] unless attribute
147 
148   extensions = unpack_extension_request(attribute)
149 
150   index = -1
151   extensions.map do |ext_values|
152     index += 1
153 
154     value = ext_value_to_ruby_value(ext_values)
155 
156     # OK, turn that into an extension, to unpack the content.  Lovely that
157     # we have to swap the order of arguments to the underlying method, or
158     # perhaps that the ASN.1 representation chose to pack them in a
159     # strange order where the optional component comes *earlier* than the
160     # fixed component in the sequence.
161     case ext_values.length
162     when 2
163       {"oid" => ext_values[0].value, "value" => value}
164     when 3
165       {"oid" => ext_values[0].value, "value" => value, "critical" => ext_values[1].value}
166     else
167       raise Puppet::Error, _("In %{attr}, expected extension record %{index} to have two or three items, but found %{count}") % { attr: attribute.oid, index: index, count: ext_values.length }
168     end
169   end
170 end
subject_alt_names() click to toggle source
    # File lib/puppet/ssl/certificate_request.rb
172 def subject_alt_names
173   @subject_alt_names ||= request_extensions.
174     select {|x| x["oid"] == "subjectAltName" }.
175     map {|x| x["value"].split(/\s*,\s*/) }.
176     flatten.
177     sort.
178     uniq
179 end

Private Instance Methods

add_csr_attributes(csr, csr_attributes) click to toggle source
    # File lib/puppet/ssl/certificate_request.rb
214 def add_csr_attributes(csr, csr_attributes)
215   csr_attributes.each do |oid, value|
216     begin
217       if PRIVATE_CSR_ATTRIBUTES.include? oid
218         raise ArgumentError, _("Cannot specify CSR attribute %{oid}: conflicts with internally used CSR attribute") % { oid: oid }
219       end
220 
221       encoded = OpenSSL::ASN1::PrintableString.new(value.to_s)
222 
223       attr_set = OpenSSL::ASN1::Set.new([encoded])
224       csr.add_attribute(OpenSSL::X509::Attribute.new(oid, attr_set))
225       Puppet.debug("Added csr attribute: #{oid} => #{attr_set.inspect}")
226     rescue OpenSSL::X509::AttributeError => e
227       raise Puppet::Error, _("Cannot create CSR with attribute %{oid}: %{message}") % { oid: oid, message: e.message }, e.backtrace
228     end
229   end
230 end
extension_request_attribute(options) click to toggle source

@api private

    # File lib/puppet/ssl/certificate_request.rb
237 def extension_request_attribute(options)
238   extensions = []
239 
240   if options[:extension_requests]
241     options[:extension_requests].each_pair do |oid, value|
242       begin
243         if PRIVATE_EXTENSIONS.include? oid
244           raise Puppet::Error, _("Cannot specify CSR extension request %{oid}: conflicts with internally used extension request") % { oid: oid }
245         end
246 
247         ext = OpenSSL::X509::Extension.new(oid, OpenSSL::ASN1::UTF8String.new(value.to_s).to_der, false)
248         extensions << ext
249       rescue OpenSSL::X509::ExtensionError => e
250         raise Puppet::Error, _("Cannot create CSR with extension request %{oid}: %{message}") % { oid: oid, message: e.message }, e.backtrace
251       end
252     end
253   end
254 
255   if options[:dns_alt_names]
256     raw_names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name]
257 
258     parsed_names = raw_names.map do |name|
259       if !name.start_with?("IP:") && !name.start_with?("DNS:")
260         "DNS:#{name}"
261       else
262         name
263       end
264     end.sort.uniq.join(", ")
265 
266     alt_names_ext = extension_factory.create_extension("subjectAltName", parsed_names, false)
267 
268     extensions << alt_names_ext
269   end
270 
271   unless extensions.empty?
272     seq = OpenSSL::ASN1::Sequence(extensions)
273     ext_req = OpenSSL::ASN1::Set([seq])
274     OpenSSL::X509::Attribute.new("extReq", ext_req)
275   end
276 end
unpack_extension_request(attribute) click to toggle source

Unpack the extReq attribute into an array of Extensions.

The extension request attribute is structured like `Set[Sequence]` where the outer Set only contains a single sequence.

In addition the Ruby implementation of ASN1 requires that all ASN1 values contain a single value, so Sets and Sequence have to contain an array that in turn holds the elements. This is why we have to unpack an array every time we unpack a Set/Seq.

@see tools.ietf.org/html/rfc2985#ref-10 5.4.2 CSR Extension Request structure @see tools.ietf.org/html/rfc5280 4.1 Certificate Extension structure

@api private

@param attribute [OpenSSL::X509::Attribute] The X509 extension request

@return [Array<Array<Object>>] A array of arrays containing the extension

OID the critical state if present, and the extension value.
    # File lib/puppet/ssl/certificate_request.rb
298 def unpack_extension_request(attribute)
299 
300   unless attribute.value.is_a? OpenSSL::ASN1::Set
301     raise Puppet::Error, _("In %{attr}, expected Set but found %{klass}") % { attr: attribute.oid, klass: attribute.value.class }
302   end
303 
304   unless attribute.value.value.is_a? Array
305     raise Puppet::Error, _("In %{attr}, expected Set[Array] but found %{klass}") % { attr: attribute.oid, klass: attribute.value.value.class }
306   end
307 
308   unless attribute.value.value.size == 1
309     raise Puppet::Error, _("In %{attr}, expected Set[Array] with one value but found %{count} elements") % { attr: attribute.oid, count: attribute.value.value.size }
310   end
311 
312   unless attribute.value.value.first.is_a? OpenSSL::ASN1::Sequence
313     raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[...]]], but found %{klass}") % { attr: attribute.oid, klass: extension.class }
314   end
315 
316   unless attribute.value.value.first.value.is_a? Array
317     raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[Array[...]]]], but found %{klass}") % { attr: attribute.oid, klass: extension.value.class }
318   end
319 
320   extensions = attribute.value.value.first.value
321 
322   extensions.map(&:value)
323 end