Class: Puppet::SSL::CertificateRequest

Inherits:
Base show all
Extended by:
Indirector
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.

Defined Under Namespace

Classes: File, Memory, Rest

Constant Summary

Constants included from Indirector

Indirector::BadNameRegexp

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 included from Indirector

configure_routes, indirects

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.


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

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


203
204
205
206
207
208
209
210
211
# File 'lib/puppet/ssl/certificate_request.rb', line 203

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


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

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


44
45
46
# File 'lib/puppet/ssl/certificate_request.rb', line 44

def extension_factory
  @ef ||= OpenSSL::X509::ExtensionFactory.new
end

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

Create a certificate request with our system settings.

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.

Raises:

  • (Puppet::Error)

    If the generated CSR signature couldn't be verified


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

def generate(key, options = {})
  Puppet.info _("Creating a new SSL certificate request for %{name}") % { name: name }

  # Support either an actual SSL key, or a Puppet key.
  key = key.content if key.is_a?(Puppet::SSL::Key)

  # 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

#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.

Raises:


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

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


183
184
185
186
187
188
189
190
# File 'lib/puppet/ssl/certificate_request.rb', line 183

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