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/2000/09/xmldsig#rsa-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: 'http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411',
    name: 'GOST R 34.11-94'
  },
  # GOST R 34-11 2012 256 bit
  gostr34112012_256: {
    id: 'urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256',
    name: 'GOST R 34.11-2012 256',
  },
}.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.9.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') },
  },
  # GOST R 34-11 2012 256 bit
  gostr34112012_256: {
    name: 'GOST R 34.11-2012 256',
    id: 'urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256',
    digester: lambda { begin OpenSSL::Digest.new('streebog256') rescue OpenSSL::Digest.new('md_gost12_256') end },
  },
}.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.



64
65
66
67
68
69
70
71
72
# File 'lib/signer.rb', line 64

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
  self.signature_digest_algorithm = :sha1
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



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

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

#security_token_idObject



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

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



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

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>



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

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



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

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

#canonicalize_algorithmObject



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

def canonicalize_algorithm
  @canonicalize_algorithm[:value]
end

#canonicalize_algorithm=(algorithm) ⇒ Object



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

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

#canonicalize_idObject



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

def canonicalize_id
  @canonicalize_algorithm[:id]
end

#canonicalize_nameObject



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

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>


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

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?
  else
    id = options[:id] || target_node['Id']
  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)

  # create reference + transforms node
  transform!(transforms_node, options)

  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.



95
96
97
# File 'lib/signer.rb', line 95

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.



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

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



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/signer.rb', line 344

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.



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

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=



114
115
116
117
# File 'lib/signer.rb', line 114

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>



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

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



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

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>



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

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