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
34 def self.supported_formats
35   [:s]
36 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
190 def custom_attributes
191   x509_attributes = @content.attributes.reject do |attr|
192     PRIVATE_CSR_ATTRIBUTES.include? attr.oid
193   end
194 
195   x509_attributes.map do |attr|
196     {"oid" => attr.oid, "value" => attr.value.value.first.value}
197   end
198 end
ext_value_to_ruby_value(asn1_arr) click to toggle source
    # File lib/puppet/ssl/certificate_request.rb
 99 def ext_value_to_ruby_value(asn1_arr)
100   # A list of ASN1 types than can't be directly converted to a Ruby type
101   @non_convertible ||= [OpenSSL::ASN1::EndOfContent,
102                         OpenSSL::ASN1::BitString,
103                         OpenSSL::ASN1::Null,
104                         OpenSSL::ASN1::Enumerated,
105                         OpenSSL::ASN1::UTCTime,
106                         OpenSSL::ASN1::GeneralizedTime,
107                         OpenSSL::ASN1::Sequence,
108                         OpenSSL::ASN1::Set]
109 
110   begin
111     # Attempt to decode the extension's DER data located in the original OctetString
112     asn1_val = OpenSSL::ASN1.decode(asn1_arr.last.value)
113   rescue OpenSSL::ASN1::ASN1Error
114     # This is to allow supporting the old-style of not DER encoding trusted facts
115     return asn1_arr.last.value
116   end
117 
118   # If the extension value can not be directly converted to an atomic Ruby
119   # type, use the original ASN1 value. This is needed to work around a bug
120   # in Ruby's OpenSSL library which doesn't convert the value of unknown
121   # extension OIDs properly. See PUP-3560
122   if @non_convertible.include?(asn1_val.class) then
123     # Allows OpenSSL to take the ASN1 value and turn it into something Ruby understands
124     OpenSSL::X509::Extension.new(asn1_arr.first.value, asn1_val.to_der).value
125   else
126     asn1_val.value
127   end
128 end
extension_factory() click to toggle source
   # File lib/puppet/ssl/certificate_request.rb
38 def extension_factory
39   @ef ||= OpenSSL::X509::ExtensionFactory.new
40 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
57 def generate(key, options = {})
58   Puppet.info _("Creating a new SSL certificate request for %{name}") % { name: name }
59 
60   # If we're a CSR for the CA, then use the real ca_name, rather than the
61   # fake 'ca' name.  This is mostly for backward compatibility with 0.24.x,
62   # but it's also just a good idea.
63   common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name
64 
65   csr = OpenSSL::X509::Request.new
66   csr.version = 0
67   csr.subject = OpenSSL::X509::Name.new([["CN", common_name]])
68 
69   csr.public_key = if key.is_a?(OpenSSL::PKey::EC)
70                      # EC#public_key doesn't follow the PKey API,
71                      # see https://github.com/ruby/openssl/issues/29
72                      key
73                    else
74                      key.public_key
75                    end
76 
77   if options[:csr_attributes]
78     add_csr_attributes(csr, options[:csr_attributes])
79   end
80 
81   if (ext_req_attribute = extension_request_attribute(options))
82     csr.add_attribute(ext_req_attribute)
83   end
84 
85   signer = Puppet::SSL::CertificateSigner.new
86   signer.sign(csr, key)
87 
88   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)
89 
90   @content = csr
91 
92   # we won't be able to get the digest on jruby
93   if @content.signature_algorithm
94     Puppet.info _("Certificate Request fingerprint (%{digest}): %{hex_digest}") % { digest: digest.name, hex_digest: digest.to_hex }
95   end
96   @content
97 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
137 def request_extensions
138   raise Puppet::Error, _("CSR needs content to extract fields") unless @content
139 
140   # Prefer the standard extReq, but accept the Microsoft specific version as
141   # a fallback, if the standard version isn't found.
142   attribute   = @content.attributes.find {|x| x.oid == "extReq" }
143   attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" }
144   return [] unless attribute
145 
146   extensions = unpack_extension_request(attribute)
147 
148   index = -1
149   extensions.map do |ext_values|
150     index += 1
151 
152     value = ext_value_to_ruby_value(ext_values)
153 
154     # OK, turn that into an extension, to unpack the content.  Lovely that
155     # we have to swap the order of arguments to the underlying method, or
156     # perhaps that the ASN.1 representation chose to pack them in a
157     # strange order where the optional component comes *earlier* than the
158     # fixed component in the sequence.
159     case ext_values.length
160     when 2
161       {"oid" => ext_values[0].value, "value" => value}
162     when 3
163       {"oid" => ext_values[0].value, "value" => value, "critical" => ext_values[1].value}
164     else
165       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 }
166     end
167   end
168 end
subject_alt_names() click to toggle source
    # File lib/puppet/ssl/certificate_request.rb
170 def subject_alt_names
171   @subject_alt_names ||= request_extensions.
172     select {|x| x["oid"] == "subjectAltName" }.
173     map {|x| x["value"].split(/\s*,\s*/) }.
174     flatten.
175     sort.
176     uniq
177 end

Private Instance Methods

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

@api private

    # File lib/puppet/ssl/certificate_request.rb
235 def extension_request_attribute(options)
236   extensions = []
237 
238   if options[:extension_requests]
239     options[:extension_requests].each_pair do |oid, value|
240       begin
241         if PRIVATE_EXTENSIONS.include? oid
242           raise Puppet::Error, _("Cannot specify CSR extension request %{oid}: conflicts with internally used extension request") % { oid: oid }
243         end
244 
245         ext = OpenSSL::X509::Extension.new(oid, OpenSSL::ASN1::UTF8String.new(value.to_s).to_der, false)
246         extensions << ext
247       rescue OpenSSL::X509::ExtensionError => e
248         raise Puppet::Error, _("Cannot create CSR with extension request %{oid}: %{message}") % { oid: oid, message: e.message }, e.backtrace
249       end
250     end
251   end
252 
253   if options[:dns_alt_names]
254     raw_names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name]
255 
256     parsed_names = raw_names.map do |name|
257       if !name.start_with?("IP:") && !name.start_with?("DNS:")
258         "DNS:#{name}"
259       else
260         name
261       end
262     end.sort.uniq.join(", ")
263 
264     alt_names_ext = extension_factory.create_extension("subjectAltName", parsed_names, false)
265 
266     extensions << alt_names_ext
267   end
268 
269   unless extensions.empty?
270     seq = OpenSSL::ASN1::Sequence(extensions)
271     ext_req = OpenSSL::ASN1::Set([seq])
272     OpenSSL::X509::Attribute.new("extReq", ext_req)
273   end
274 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
296 def unpack_extension_request(attribute)
297 
298   unless attribute.value.is_a? OpenSSL::ASN1::Set
299     raise Puppet::Error, _("In %{attr}, expected Set but found %{klass}") % { attr: attribute.oid, klass: attribute.value.class }
300   end
301 
302   unless attribute.value.value.is_a? Array
303     raise Puppet::Error, _("In %{attr}, expected Set[Array] but found %{klass}") % { attr: attribute.oid, klass: attribute.value.value.class }
304   end
305 
306   unless attribute.value.value.size == 1
307     raise Puppet::Error, _("In %{attr}, expected Set[Array] with one value but found %{count} elements") % { attr: attribute.oid, count: attribute.value.value.size }
308   end
309 
310   unless attribute.value.value.first.is_a? OpenSSL::ASN1::Sequence
311     raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[...]]], but found %{klass}") % { attr: attribute.oid, klass: extension.class }
312   end
313 
314   unless attribute.value.value.first.value.is_a? Array
315     raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[Array[...]]]], but found %{klass}") % { attr: attribute.oid, klass: extension.value.class }
316   end
317 
318   extensions = attribute.value.value.first.value
319 
320   extensions.map(&:value)
321 end