Class: Puppet::SSL::CertificateRequest

Inherits:
Base show all
Defined in:
lib/puppet/ssl/certificate_request.rb

Overview

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.

Constant Summary

Constants inherited from Base

Base::SEPARATOR, Base::VALID_CERTNAME

Instance Attribute Summary

Attributes inherited from Base

#content, #name

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#digest, #digest_algorithm, #fingerprint, from_instance, from_multiple_s, from_s, #initialize, name_from_subject, #read, #to_data_hash, to_multiple_s, #to_s, #to_text, validate_certname, wrapped_class, wraps

Constructor Details

This class inherits a constructor from Puppet::SSL::Base

Class Method Details

.supported_formatsObject

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



35
36
37
# File 'lib/puppet/ssl/certificate_request.rb', line 35

def self.supported_formats
  [:s]
end

Instance Method Details

#custom_attributesHash<String, String>

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

Returns:

  • (Hash<String, String>)

See Also:



193
194
195
196
197
198
199
200
201
# File 'lib/puppet/ssl/certificate_request.rb', line 193

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

#ext_value_to_ruby_value(asn1_arr) ⇒ Object



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
# File 'lib/puppet/ssl/certificate_request.rb', line 102

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

#extension_factoryObject



39
40
41
42
43
# File 'lib/puppet/ssl/certificate_request.rb', line 39

def extension_factory
  # rubocop:disable Naming/MemoizedInstanceVariableName
  @ef ||= OpenSSL::X509::ExtensionFactory.new
  # rubocop:enable Naming/MemoizedInstanceVariableName
end

#generate(key, options = {}) ⇒ OpenSSL::X509::Request

Create a certificate request with our system settings.

Parameters:

  • key (OpenSSL::X509::Key)

    The private key associated with this CSR.

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :dns_alt_names (String)

    A comma separated list of Subject Alternative Names to include in the CSR extension request.

  • :csr_attributes (Hash<String, String, Array<String>>)

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

  • :extension_requests (Array<String, String>)

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

Returns:

Raises:

  • (Puppet::Error)

    If the generated CSR signature couldn’t be verified



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
# File 'lib/puppet/ssl/certificate_request.rb', line 60

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
                     key
                   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

#request_extensionsArray<Hash{String => String}>

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.

hashes, with key/value pairs for the extension’s oid, its value, and optionally its critical state.

Returns:

  • (Array<Hash{String => String}>)

    An array of two or three element

Raises:



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
# File 'lib/puppet/ssl/certificate_request.rb', line 140

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

#subject_alt_namesObject



173
174
175
176
177
178
179
180
# File 'lib/puppet/ssl/certificate_request.rb', line 173

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