Class: Signer

Inherits:
Object
  • Object
show all
Defined in:
lib/signer.rb,
lib/signer/version.rb,
lib/signer/digester.rb

Defined Under Namespace

Classes: Digester

Constant Summary collapse

WSU_NAMESPACE =
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'.freeze
WSSE_NAMESPACE =
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'.freeze
DS_NAMESPACE =
'http://www.w3.org/2000/09/xmldsig#'.freeze
SIGNATURE_ALGORITHM =
{
  # SHA 1
  sha1: {
    id: 'http://www.w3.org/2001/04/xmlenc#sha1',
    name: 'SHA1'
  },
  # SHA 256
  sha256: {
    id: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
    name: 'SHA256'
  },
  # SHA512
  sha512: {
    id: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
    name: 'SHA512'
  },
  # GOST R 34-11 94
  gostr3411: {
    id: 'https://www.w3.org/2001/04/xmldsig-more#rsa-gostr3411',
    name: 'GOST R 34.11-94'
  }
}.freeze
CANONICALIZE_ALGORITHM =
{
  c14n_exec_1_0: {
    name: 'c14n execlusive 1.0',
    value: Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
    id: 'http://www.w3.org/2001/10/xml-exc-c14n#'
  },
  c14n_1_0: {
    name: 'c14n 1.0',
    value: Nokogiri::XML::XML_C14N_1_0,
    id: 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
  },
  c14n_1_1: {
    name: 'c14n 1.1',
    value: Nokogiri::XML::XML_C14N_1_1,
    id: 'https://www.w3.org/TR/2008/REC-xml-c14n11-20080502/'
  }
}.freeze
VERSION =
'1.8.0'
DIGEST_ALGORITHMS =

Digest algorithms supported “out of the box”

{
  # SHA 1
  sha1: {
    name: 'SHA1',
    id: 'http://www.w3.org/2000/09/xmldsig#sha1',
    digester: lambda { OpenSSL::Digest::SHA1.new },
  },
  # SHA 256
  sha256: {
    name: 'SHA256',
    id: 'http://www.w3.org/2001/04/xmlenc#sha256',
    digester: lambda { OpenSSL::Digest::SHA256.new },
  },
  # SHA512
  sha512: {
    name: 'SHA512',
    id: 'http://www.w3.org/2001/04/xmlenc#sha512',
    digester: lambda { OpenSSL::Digest::SHA512.new },
  },
  # GOST R 34-11 94
  gostr3411: {
    name: 'GOST R 34.11-94',
    id: 'http://www.w3.org/2001/04/xmldsig-more#gostr3411',
    digester: lambda { OpenSSL::Digest.new('md_gost94') },
  },
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document, noblanks: true, wss: true, canonicalize_algorithm: :c14n_exec_1_0) ⇒ Signer

Returns a new instance of Signer.



59
60
61
62
63
64
65
66
67
# File 'lib/signer.rb', line 59

def initialize(document, noblanks: true, wss: true, canonicalize_algorithm: :c14n_exec_1_0)
  self.document = Nokogiri::XML(document.to_s) do |config|
    config.noblanks if noblanks
  end
  self.digest_algorithm = :sha1
  self.wss = wss
  self.canonicalize_algorithm = canonicalize_algorithm
  set_default_signature_method!
end

Instance Attribute Details

#certObject

Returns the value of attribute cert.



11
12
13
# File 'lib/signer.rb', line 11

def cert
  @cert
end

#documentObject

Returns the value of attribute document.



10
11
12
# File 'lib/signer.rb', line 10

def document
  @document
end

#ds_namespace_prefixObject

Returns the value of attribute ds_namespace_prefix.



10
11
12
# File 'lib/signer.rb', line 10

def ds_namespace_prefix
  @ds_namespace_prefix
end

#private_keyObject

Returns the value of attribute private_key.



10
11
12
# File 'lib/signer.rb', line 10

def private_key
  @private_key
end

#security_nodeObject



134
135
136
# File 'lib/signer.rb', line 134

def security_node
  @security_node ||= wss? ? document.xpath('//wsse:Security', wsse: WSSE_NAMESPACE).first : ''
end

#security_token_idObject



130
131
132
# File 'lib/signer.rb', line 130

def security_token_id
  @security_token_id ||= wss? ? "uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1" : ""
end

#signature_algorithm_idObject

Returns the value of attribute signature_algorithm_id.



10
11
12
# File 'lib/signer.rb', line 10

def signature_algorithm_id
  @signature_algorithm_id
end

#signature_nodeObject

<Signature xmlns=“www.w3.org/2000/09/xmldsig#”>



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/signer.rb', line 143

def signature_node
  @signature_node ||= begin
    @signature_node = security_node.at_xpath('ds:Signature', ds: DS_NAMESPACE)
    unless @signature_node
      @signature_node = Nokogiri::XML::Node.new('Signature', document)
      set_namespace_for_node(@signature_node, DS_NAMESPACE, ds_namespace_prefix)
      security_node.add_child(@signature_node)
    end
    @signature_node
  end
end

#wssObject

Returns the value of attribute wss.



10
11
12
# File 'lib/signer.rb', line 10

def wss
  @wss
end

Instance Method Details

#binary_security_token_nodeObject

<o:BinarySecurityToken u:Id=“” ValueType=“docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3” EncodingType=“docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary”>

...

</o:BinarySecurityToken> <SignedInfo>

...

</SignedInfo> <KeyInfo>

<o:SecurityTokenReference>
  <o:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" URI="#uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1"/>
</o:SecurityTokenReference>

</KeyInfo>



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/signer.rb', line 189

def binary_security_token_node
  return unless wss?
  node = document.at_xpath('wsse:BinarySecurityToken', wsse: WSSE_NAMESPACE)
  unless node
    node = Nokogiri::XML::Node.new('BinarySecurityToken', document)
    node['ValueType']    = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3'
    node['EncodingType'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'
    node.content = Base64.encode64(cert.to_der).gsub("\n", '')
    signature_node.add_previous_sibling(node)
    wsse_ns = namespace_prefix(node, WSSE_NAMESPACE, 'wsse')
    wsu_ns = namespace_prefix(node, WSU_NAMESPACE, 'wsu')
    node["#{wsu_ns}:Id"] = security_token_id
    key_info_node = Nokogiri::XML::Node.new('KeyInfo', document)
    security_token_reference_node = Nokogiri::XML::Node.new("#{wsse_ns}:SecurityTokenReference", document)
    key_info_node.add_child(security_token_reference_node)
    set_namespace_for_node(key_info_node, DS_NAMESPACE, ds_namespace_prefix)
    reference_node = Nokogiri::XML::Node.new("#{wsse_ns}:Reference", document)
    reference_node['ValueType'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3'
    reference_node['URI'] = "##{security_token_id}"
    security_token_reference_node.add_child(reference_node)
    signed_info_node.add_next_sibling(key_info_node)
  end
  node
end

#canonicalize(node = document, inclusive_namespaces = nil, algorithm: canonicalize_algorithm) ⇒ Object



138
139
140
# File 'lib/signer.rb', line 138

def canonicalize(node = document, inclusive_namespaces=nil, algorithm: canonicalize_algorithm)
  node.canonicalize(algorithm, inclusive_namespaces, nil)
end

#canonicalize_algorithmObject



81
82
83
# File 'lib/signer.rb', line 81

def canonicalize_algorithm
  @canonicalize_algorithm[:value]
end

#canonicalize_algorithm=(algorithm) ⇒ Object



85
86
87
# File 'lib/signer.rb', line 85

def canonicalize_algorithm=(algorithm)
  @canonicalize_algorithm = CANONICALIZE_ALGORITHM[algorithm]
end

#canonicalize_idObject



77
78
79
# File 'lib/signer.rb', line 77

def canonicalize_id
  @canonicalize_algorithm[:id]
end

#canonicalize_nameObject



73
74
75
# File 'lib/signer.rb', line 73

def canonicalize_name
  @canonicalize_algorithm[:name]
end

#digest!(target_node, options = {}) ⇒ Object

Digests some target_node, which integrity you wish to track. Any changes in digested node will invalidate signed message. All digest should be calculated before signing.

Available options:

  • :id

    Id for the node, if you don’t want to use automatically calculated one

  • :inclusive_namespaces

    Array of namespace prefixes which definitions should be added to node during canonicalization

  • :enveloped
  • :ref_type

    add ‘Type` attribute to Reference node, if ref_type is not nil

Example of XML that will be inserted in message for call like digest!(node, inclusive_namespaces: ['soap']):

<Reference URI="#_0">
  <Transforms>
    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
      <ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="soap" />
    </Transform>
  </Transforms>
  <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
  <DigestValue>aeqXriJuUCk4tPNPAGDXGqHj6ao=</DigestValue>
</Reference>


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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/signer.rb', line 286

def digest!(target_node, options = {})
  if wss?
    wsu_ns = namespace_prefix(target_node, WSU_NAMESPACE)
    current_id = target_node["#{wsu_ns}:Id"]  if wsu_ns
    id = options[:id] || current_id || "_#{Digest::SHA1.hexdigest(target_node.to_s)}"
    unless id.to_s.empty?
      wsu_ns ||= namespace_prefix(target_node, WSU_NAMESPACE, 'wsu')
      target_node["#{wsu_ns}:Id"] = id.to_s
    end
  elsif target_node['Id'].nil?
    id = options[:id] || "_#{Digest::SHA1.hexdigest(target_node.to_s)}"
    target_node['Id'] = id.to_s unless id.empty?
  end

  target_canon = canonicalize(target_node, options[:inclusive_namespaces])
  target_digest = Base64.encode64(@digester.digest(target_canon)).strip

  reference_node = Nokogiri::XML::Node.new('Reference', document)
  reference_node['URI'] = id.to_s.size > 0 ? "##{id}" : ""
  reference_node['Type'] = options[:ref_type] if options[:ref_type]

  signed_info_node.add_child(reference_node)
  set_namespace_for_node(reference_node, DS_NAMESPACE, ds_namespace_prefix)

  transforms_node = Nokogiri::XML::Node.new('Transforms', document)
  reference_node.add_child(transforms_node) unless options[:no_transform]
  set_namespace_for_node(transforms_node, DS_NAMESPACE, ds_namespace_prefix)

  transform_node = Nokogiri::XML::Node.new('Transform', document)
  set_namespace_for_node(transform_node, DS_NAMESPACE, ds_namespace_prefix)
  if options[:enveloped]
    transform_node['Algorithm'] = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
  else
    transform_node['Algorithm'] = 'http://www.w3.org/2001/10/xml-exc-c14n#'
  end

  if options[:inclusive_namespaces]
    inclusive_namespaces_node = Nokogiri::XML::Node.new('ec:InclusiveNamespaces', document)
    inclusive_namespaces_node.add_namespace_definition('ec', transform_node['Algorithm'])
    inclusive_namespaces_node['PrefixList'] = options[:inclusive_namespaces].join(' ')
    transform_node.add_child(inclusive_namespaces_node)
  end

  transforms_node.add_child(transform_node)

  digest_method_node = Nokogiri::XML::Node.new('DigestMethod', document)
  digest_method_node['Algorithm'] = @digester.digest_id

  reference_node.add_child(digest_method_node)
  set_namespace_for_node(digest_method_node, DS_NAMESPACE, ds_namespace_prefix)

  digest_value_node = Nokogiri::XML::Node.new('DigestValue', document)
  digest_value_node.content = target_digest
  reference_node.add_child(digest_value_node)
  set_namespace_for_node(digest_value_node, DS_NAMESPACE, ds_namespace_prefix)
  self
end

#digest_algorithmObject

Return symbol name for supported digest algorithms and string name for custom ones.



90
91
92
# File 'lib/signer.rb', line 90

def digest_algorithm
  @digester.symbol || @digester.digest_name
end

#digest_algorithm=(algorithm) ⇒ Object

Allows to change algorithm for node digesting (default is SHA1).

You may pass either a one of :sha1, :sha256 or :gostr3411 symbols or Hash with keys :id with a string, which will denote algorithm in XML Reference tag and :digester with instance of class with interface compatible with OpenSSL::Digest class.



99
100
101
# File 'lib/signer.rb', line 99

def digest_algorithm=(algorithm)
  @digester = Signer::Digester.new(algorithm)
end

#sign!(options = {}) ⇒ Object

Sign document with provided certificate, private key and other options

This should be very last action before calling to_xml, all the required nodes should be digested with digest! before signing.

Available options:

  • :security_token

    Serializes certificate in DER format, encodes it with Base64 and inserts it within <BinarySecurityToken> tag

  • :issuer_serial
  • :issuer_in_security_token
  • :inclusive_namespaces

    Array of namespace prefixes which definitions should be added to signed info node during canonicalization



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/signer.rb', line 355

def sign!(options = {})
  if options[:security_token]
    binary_security_token_node
  end

  if options[:issuer_serial]
    x509_data_node(options[:issuer_in_security_token])
  end

  if options[:inclusive_namespaces]
    c14n_method_node = signed_info_node.at_xpath('ds:CanonicalizationMethod', ds: DS_NAMESPACE)
    inclusive_namespaces_node = Nokogiri::XML::Node.new('ec:InclusiveNamespaces', document)
    inclusive_namespaces_node.add_namespace_definition('ec', c14n_method_node['Algorithm'])
    inclusive_namespaces_node['PrefixList'] = options[:inclusive_namespaces].join(' ')
    c14n_method_node.add_child(inclusive_namespaces_node)
  end

  signed_info_canon = canonicalize(signed_info_node, options[:inclusive_namespaces])

  signature = private_key.sign(@sign_digester.digester, signed_info_canon)
  signature_value_digest = Base64.encode64(signature).delete("\n")

  signature_value_node = Nokogiri::XML::Node.new('SignatureValue', document)
  signature_value_node.content = signature_value_digest
  signed_info_node.add_next_sibling(signature_value_node)
  set_namespace_for_node(signature_value_node, DS_NAMESPACE, ds_namespace_prefix)
  self
end

#signature_digest_algorithmObject

Return symbol name for supported digest algorithms and string name for custom ones.



104
105
106
# File 'lib/signer.rb', line 104

def signature_digest_algorithm
  @sign_digester.symbol || @sign_digester.digest_name
end

#signature_digest_algorithm=(algorithm) ⇒ Object

Allows to change digesting algorithm for signature creation. Same as digest_algorithm=



109
110
111
112
# File 'lib/signer.rb', line 109

def signature_digest_algorithm=(algorithm)
  @sign_digester = Signer::Digester.new(algorithm)
  self.signature_algorithm_id = SIGNATURE_ALGORITHM[algorithm][:id]
end

#signed_info_nodeObject

<SignedInfo>

<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
...

</SignedInfo>



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/signer.rb', line 160

def signed_info_node
  node = signature_node.at_xpath('ds:SignedInfo', ds: DS_NAMESPACE)
  unless node
    node = Nokogiri::XML::Node.new('SignedInfo', document)
    signature_node.add_child(node)
    set_namespace_for_node(node, DS_NAMESPACE, ds_namespace_prefix)
    canonicalization_method_node = Nokogiri::XML::Node.new('CanonicalizationMethod', document)
    canonicalization_method_node['Algorithm'] = canonicalize_id
    node.add_child(canonicalization_method_node)
    set_namespace_for_node(canonicalization_method_node, DS_NAMESPACE, ds_namespace_prefix)
    signature_method_node = Nokogiri::XML::Node.new('SignatureMethod', document)
    signature_method_node['Algorithm'] = self.signature_algorithm_id
    node.add_child(signature_method_node)
    set_namespace_for_node(signature_method_node, DS_NAMESPACE, ds_namespace_prefix)
  end
  node
end

#to_xmlObject



69
70
71
# File 'lib/signer.rb', line 69

def to_xml
  document.to_xml(save_with: 0)
end

#x509_data_node(issuer_in_security_token = false) ⇒ Object

<KeyInfo> <SecurityTokenReference> (optional)

<X509Data>
  <X509IssuerSerial>
    <X509IssuerName>System.Security.Cryptography.X509Certificates.X500DistinguishedName</X509IssuerName>
    <X509SerialNumber>13070789</X509SerialNumber>
  </X509IssuerSerial>
  <X509Certificate>MIID+jCCAuKgAwIBAgIEAMdxxTANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQGEwJTRTEeMBwGA1UEChMVTm9yZGVhIEJhbmsgQUIgKHB1YmwpMScwJQYDVQQDEx5Ob3JkZWEgcm9sZS1jZXJ0aWZpY2F0ZXMgQ0EgMDExFDASBgNVBAUTCzUxNjQwNi0wMTIwMB4XDTA5MDYxMTEyNTAxOVoXDTExMDYxMTEyNTAxOVowcjELMAkGA1UEBhMCU0UxIDAeBgNVBAMMF05vcmRlYSBEZW1vIENlcnRpZmljYXRlMRQwEgYDVQQEDAtDZXJ0aWZpY2F0ZTEUMBIGA1UEKgwLTm9yZGVhIERlbW8xFTATBgNVBAUTDDAwOTU1NzI0Mzc3MjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwcgz5AzbxTbsCE51No7fPnSqmQBIMW9OiPkiHotwYQTl+H9qwDvQRyBqHN26tnw7hNvEShd1ZRGUg4drMEXDV5CmKqsAevs9lauWDaHnGKPNHZJ1hNNYXHwymksEz5zMnG8eqRdhb4vOV2FzreJeYpsgx31Bv0aTofHcHVz4uGcCAwEAAaOCASAwggEcMAkGA1UdEwQCMAAwEQYDVR0OBAoECEj6Y9/vU03WMBMGA1UdIAQMMAowCAYGKoVwRwEDMBMGA1UdIwQMMAqACEIFjfLBeTpRMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3Aubm9yZGVhLnNlL1JDQTAxMA4GA1UdDwEB/wQEAwIGQDCBiAYDVR0fBIGAMH4wfKB6oHiGdmxkYXA6Ly9sZGFwLm5iLnNlL2NuPU5vcmRlYSUyMHJvbGUtY2VydGlmaWNhdGVzJTIwQ0ElMjAwMSxvPU5vcmRlYSUyMEJhbmslMjBBQiUyMChwdWJsKSxjPVNFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwDQYJKoZIhvcNAQEFBQADggEBAEXUv87VpHk51y3TqkMb1MYDqeKvQRE1cNcvhEJhIzdDpXMA9fG0KqvSTT1e0ZI2r78mXDvtTZnpic44jX2XMSmKO6n+1taAXq940tJUhF4arYMUxwDKOso0Doanogug496gipqMlpLgvIhGt06sWjNrvHzp2eGydUFdCsLr2ULqbDcut7g6eMcmrsnrOntjEU/J3hO8gyCeldJ+fI81qarrK/I0MZLR5LWCyVG/SKduoxHLX7JohsbIGyK1qAh9fi8l6X1Rcu80v5inpu71E/DnjbkAZBo7vsj78zzdk7KNliBIqBcIszdJ3dEHRWSI7FspRxyiR0NDm4lpyLwFtfw=</X509Certificate>
</X509Data>

</SecurityTokenReference> (optional) </KeyInfo>



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
# File 'lib/signer.rb', line 225

def x509_data_node(issuer_in_security_token = false)
  issuer_name_node   = Nokogiri::XML::Node.new('X509IssuerName', document)
  issuer_name_node.content = cert.issuer.to_s(OpenSSL::X509::Name::RFC2253)

  issuer_number_node = Nokogiri::XML::Node.new('X509SerialNumber', document)
  issuer_number_node.content = cert.serial

  issuer_serial_node = Nokogiri::XML::Node.new('X509IssuerSerial', document)
  issuer_serial_node.add_child(issuer_name_node)
  issuer_serial_node.add_child(issuer_number_node)

  cetificate_node    = Nokogiri::XML::Node.new('X509Certificate', document)
  cetificate_node.content = Base64.encode64(cert.to_der).delete("\n")

  data_node          = Nokogiri::XML::Node.new('X509Data', document)
  data_node.add_child(issuer_serial_node)
  data_node.add_child(cetificate_node)

  if issuer_in_security_token
    security_token_reference_node = Nokogiri::XML::Node.new("wsse:SecurityTokenReference", document)
    security_token_reference_node.add_child(data_node)
  end

  key_info_node      = Nokogiri::XML::Node.new('KeyInfo', document)
  key_info_node.add_child(issuer_in_security_token ? security_token_reference_node : data_node)

  signed_info_node.add_next_sibling(key_info_node)

  set_namespace_for_node(key_info_node, DS_NAMESPACE, ds_namespace_prefix)
  set_namespace_for_node(security_token_reference_node, WSSE_NAMESPACE, ds_namespace_prefix) if issuer_in_security_token
  set_namespace_for_node(data_node, DS_NAMESPACE, ds_namespace_prefix)
  set_namespace_for_node(issuer_serial_node, DS_NAMESPACE, ds_namespace_prefix)
  set_namespace_for_node(cetificate_node, DS_NAMESPACE, ds_namespace_prefix)
  set_namespace_for_node(issuer_name_node, DS_NAMESPACE, ds_namespace_prefix)
  set_namespace_for_node(issuer_number_node, DS_NAMESPACE, ds_namespace_prefix)

  data_node
end