1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
|
require_relative '../../puppet/ssl/base'
require_relative '../../puppet/ssl/certificate_signer'
# 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 https://tools.ietf.org/html/rfc2985 "RFC 2985 Section 5.4.2 Extension request"
#
class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
wraps OpenSSL::X509::Request
# Because of how the format handler class is included, this
# can't be in the base class.
def self.supported_formats
[:s]
end
def extension_factory
@ef ||= OpenSSL::X509::ExtensionFactory.new
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
def generate(key, options = {})
Puppet.info _("Creating a new SSL certificate request for %{name}") % { name: name }
# If we're a CSR for the CA, then use the real ca_name, rather than the
# fake 'ca' name. This is mostly for backward compatibility with 0.24.x,
# but it's also just a good idea.
common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = OpenSSL::X509::Name.new([["CN", common_name]])
csr.public_key = if key.is_a?(OpenSSL::PKey::EC)
# EC#public_key doesn't follow the PKey API,
# see https://github.com/ruby/openssl/issues/29
point = key.public_key
pubkey = OpenSSL::PKey::EC.new(point.group)
pubkey.public_key = point
pubkey
else
key.public_key
end
if options[:csr_attributes]
add_csr_attributes(csr, options[:csr_attributes])
end
if (ext_req_attribute = extension_request_attribute(options))
csr.add_attribute(ext_req_attribute)
end
signer = Puppet::SSL::CertificateSigner.new
signer.sign(csr, key)
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)
@content = csr
# we won't be able to get the digest on jruby
if @content.signature_algorithm
Puppet.info _("Certificate Request fingerprint (%{digest}): %{hex_digest}") % { digest: digest.name, hex_digest: digest.to_hex }
end
@content
end
def ext_value_to_ruby_value(asn1_arr)
# A list of ASN1 types than can't be directly converted to a Ruby type
@non_convertible ||= [OpenSSL::ASN1::EndOfContent,
OpenSSL::ASN1::BitString,
OpenSSL::ASN1::Null,
OpenSSL::ASN1::Enumerated,
OpenSSL::ASN1::UTCTime,
OpenSSL::ASN1::GeneralizedTime,
OpenSSL::ASN1::Sequence,
OpenSSL::ASN1::Set]
begin
# Attempt to decode the extension's DER data located in the original OctetString
asn1_val = OpenSSL::ASN1.decode(asn1_arr.last.value)
rescue OpenSSL::ASN1::ASN1Error
# This is to allow supporting the old-style of not DER encoding trusted facts
return asn1_arr.last.value
end
# If the extension value can not be directly converted to an atomic Ruby
# type, use the original ASN1 value. This is needed to work around a bug
# in Ruby's OpenSSL library which doesn't convert the value of unknown
# extension OIDs properly. See PUP-3560
if @non_convertible.include?(asn1_val.class) then
# Allows OpenSSL to take the ASN1 value and turn it into something Ruby understands
OpenSSL::X509::Extension.new(asn1_arr.first.value, asn1_val.to_der).value
else
asn1_val.value
end
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.
def request_extensions
raise Puppet::Error, _("CSR needs content to extract fields") unless @content
# Prefer the standard extReq, but accept the Microsoft specific version as
# a fallback, if the standard version isn't found.
attribute = @content.attributes.find {|x| x.oid == "extReq" }
attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" }
return [] unless attribute
extensions = unpack_extension_request(attribute)
index = -1
extensions.map do |ext_values|
index += 1
value = ext_value_to_ruby_value(ext_values)
# OK, turn that into an extension, to unpack the content. Lovely that
# we have to swap the order of arguments to the underlying method, or
# perhaps that the ASN.1 representation chose to pack them in a
# strange order where the optional component comes *earlier* than the
# fixed component in the sequence.
case ext_values.length
when 2
{"oid" => ext_values[0].value, "value" => value}
when 3
{"oid" => ext_values[0].value, "value" => value, "critical" => ext_values[1].value}
else
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 }
end
end
end
def subject_alt_names
@subject_alt_names ||= request_extensions.
select {|x| x["oid"] == "subjectAltName" }.
map {|x| x["value"].split(/\s*,\s*/) }.
flatten.
sort.
uniq
end
# 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 https://tools.ietf.org/html/rfc2986 "RFC 2986 Certification Request Syntax Specification"
#
# @api public
#
# @return [Hash<String, String>]
def custom_attributes
x509_attributes = @content.attributes.reject do |attr|
PRIVATE_CSR_ATTRIBUTES.include? attr.oid
end
x509_attributes.map do |attr|
{"oid" => attr.oid, "value" => attr.value.value.first.value}
end
end
private
# 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_CSR_ATTRIBUTES = [
'extReq', '1.2.840.113549.1.9.14',
'msExtReq', '1.3.6.1.4.1.311.2.1.14',
]
def add_csr_attributes(csr, csr_attributes)
csr_attributes.each do |oid, value|
begin
if PRIVATE_CSR_ATTRIBUTES.include? oid
raise ArgumentError, _("Cannot specify CSR attribute %{oid}: conflicts with internally used CSR attribute") % { oid: oid }
end
encoded = OpenSSL::ASN1::PrintableString.new(value.to_s)
attr_set = OpenSSL::ASN1::Set.new([encoded])
csr.add_attribute(OpenSSL::X509::Attribute.new(oid, attr_set))
Puppet.debug("Added csr attribute: #{oid} => #{attr_set.inspect}")
rescue OpenSSL::X509::AttributeError => e
raise Puppet::Error, _("Cannot create CSR with attribute %{oid}: %{message}") % { oid: oid, message: e.message }, e.backtrace
end
end
end
PRIVATE_EXTENSIONS = [
'subjectAltName', '2.5.29.17',
]
# @api private
def extension_request_attribute(options)
extensions = []
if options[:extension_requests]
options[:extension_requests].each_pair do |oid, value|
begin
if PRIVATE_EXTENSIONS.include? oid
raise Puppet::Error, _("Cannot specify CSR extension request %{oid}: conflicts with internally used extension request") % { oid: oid }
end
ext = OpenSSL::X509::Extension.new(oid, OpenSSL::ASN1::UTF8String.new(value.to_s).to_der, false)
extensions << ext
rescue OpenSSL::X509::ExtensionError => e
raise Puppet::Error, _("Cannot create CSR with extension request %{oid}: %{message}") % { oid: oid, message: e.message }, e.backtrace
end
end
end
if options[:dns_alt_names]
raw_names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name]
parsed_names = raw_names.map do |name|
if !name.start_with?("IP:") && !name.start_with?("DNS:")
"DNS:#{name}"
else
name
end
end.sort.uniq.join(", ")
alt_names_ext = extension_factory.create_extension("subjectAltName", parsed_names, false)
extensions << alt_names_ext
end
unless extensions.empty?
seq = OpenSSL::ASN1::Sequence(extensions)
ext_req = OpenSSL::ASN1::Set([seq])
OpenSSL::X509::Attribute.new("extReq", ext_req)
end
end
# Unpack the extReq attribute into an array of Extensions.
#
# The extension request attribute is structured like
# `Set[Sequence[Extensions]]` 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 https://tools.ietf.org/html/rfc2985#ref-10 5.4.2 CSR Extension Request structure
# @see https://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.
def unpack_extension_request(attribute)
unless attribute.value.is_a? OpenSSL::ASN1::Set
raise Puppet::Error, _("In %{attr}, expected Set but found %{klass}") % { attr: attribute.oid, klass: attribute.value.class }
end
unless attribute.value.value.is_a? Array
raise Puppet::Error, _("In %{attr}, expected Set[Array] but found %{klass}") % { attr: attribute.oid, klass: attribute.value.value.class }
end
unless attribute.value.value.size == 1
raise Puppet::Error, _("In %{attr}, expected Set[Array] with one value but found %{count} elements") % { attr: attribute.oid, count: attribute.value.value.size }
end
unless attribute.value.value.first.is_a? OpenSSL::ASN1::Sequence
raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[...]]], but found %{klass}") % { attr: attribute.oid, klass: extension.class }
end
unless attribute.value.value.first.value.is_a? Array
raise Puppet::Error, _("In %{attr}, expected Set[Array[Sequence[Array[...]]]], but found %{klass}") % { attr: attribute.oid, klass: extension.value.class }
end
extensions = attribute.value.value.first.value
extensions.map(&:value)
end
end
|