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
Puppetcreates 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
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
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
# 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
# File lib/puppet/ssl/certificate_request.rb 38 def extension_factory 39 @ef ||= OpenSSL::X509::ExtensionFactory.new 40 end
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
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
# 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
# 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
@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 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