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