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'
WSSE_NAMESPACE =
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
VERSION =
'1.4.2'
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/xmldsig-more#rsa-sha256',
      digester: lambda { OpenSSL::Digest::SHA256.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') },
  },
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Signer

Returns a new instance of Signer.



17
18
19
20
21
# File 'lib/signer.rb', line 17

def initialize(document)
  self.document = Nokogiri::XML(document.to_s, &:noblanks)
  self.digest_algorithm = :sha1
  self.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

#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



71
72
73
# File 'lib/signer.rb', line 71

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

#security_token_idObject



67
68
69
# File 'lib/signer.rb', line 67

def security_token_id
  @security_token_id ||= "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#”>



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/signer.rb', line 80

def signature_node
  @signature_node ||= begin
    @signature_node = security_node.at_xpath('ds:Signature', ds: 'http://www.w3.org/2000/09/xmldsig#')
    unless @signature_node
      @signature_node = Nokogiri::XML::Node.new('Signature', document)
      @signature_node.default_namespace = 'http://www.w3.org/2000/09/xmldsig#'
      security_node.add_child(@signature_node)
    end
    @signature_node
  end
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>



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/signer.rb', line 123

def binary_security_token_node
  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)
    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) ⇒ Object



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

def canonicalize(node = document, inclusive_namespaces=nil)
  node.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0, inclusive_namespaces, nil) # The last argument should be exactly +nil+ to remove comments from result
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

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>


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

def digest!(target_node, options = {})
  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)}"
  if id.to_s.size > 0
    wsu_ns ||= namespace_prefix(target_node, WSU_NAMESPACE, 'wsu')
    target_node["#{wsu_ns}:Id"] = id.to_s
  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}" : ""
  signed_info_node.add_child(reference_node)

  transforms_node = Nokogiri::XML::Node.new('Transforms', document)
  reference_node.add_child(transforms_node)

  transform_node = Nokogiri::XML::Node.new('Transform', document)
  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)

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

#digest_algorithmObject

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



28
29
30
# File 'lib/signer.rb', line 28

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.



37
38
39
# File 'lib/signer.rb', line 37

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
  • :inclusive_namespaces

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



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

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

  if options[:issuer_serial]
    x509_data_node
  end

  if options[:inclusive_namespaces]
    c14n_method_node = signed_info_node.at_xpath('ds:CanonicalizationMethod', ds: 'http://www.w3.org/2000/09/xmldsig#')
    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).gsub("\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)
  self
end

#signature_digest_algorithmObject

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



42
43
44
# File 'lib/signer.rb', line 42

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=



47
48
49
# File 'lib/signer.rb', line 47

def signature_digest_algorithm=(algorithm)
  @sign_digester = Signer::Digester.new(algorithm)
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>



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/signer.rb', line 97

def signed_info_node
  node = signature_node.at_xpath('ds:SignedInfo', ds: 'http://www.w3.org/2000/09/xmldsig#')
  unless node
    node = Nokogiri::XML::Node.new('SignedInfo', document)
    signature_node.add_child(node)
    canonicalization_method_node = Nokogiri::XML::Node.new('CanonicalizationMethod', document)
    canonicalization_method_node['Algorithm'] = 'http://www.w3.org/2001/10/xml-exc-c14n#'
    node.add_child(canonicalization_method_node)
    signature_method_node = Nokogiri::XML::Node.new('SignatureMethod', document)
    signature_method_node['Algorithm'] = self.signature_algorithm_id
    node.add_child(signature_method_node)
  end
  node
end

#to_xmlObject



23
24
25
# File 'lib/signer.rb', line 23

def to_xml
  document.to_xml(:save_with => 0)
end

#x509_data_nodeObject

<KeyInfo>

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

</KeyInfo>



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

def x509_data_node
  issuer_name_node   = Nokogiri::XML::Node.new('X509IssuerName', document)
  issuer_name_node.content = "System.Security.Cryptography.X509Certificates.X500DistinguishedName"

  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).gsub("\n", '')

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

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

  signed_info_node.add_next_sibling(key_info_node)

  data_node
end