akami 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20bb54b460119624acb0737c62c1c0b82825ace9886a239e2a3cefd8461635c7
4
- data.tar.gz: c5fb01bbaf55b70be8627e2f9ee6a40cd2e6680ee2b0cee65d38eedd78961a48
3
+ metadata.gz: 184a02f0bc62b95abd62b455fcd57dd5ffdf8c7d88a89b0651f8edb566fab1db
4
+ data.tar.gz: 39adab6b78b3a9346ce22b9bc3981e13f3bc67637fbc75276a48d6e0b0f9d0c9
5
5
  SHA512:
6
- metadata.gz: 787c578411ef161d8109708b1d498743b9bf7a24e840566209f946179b9ffe4ce0d20ce2236a615973108ffb578423bad481cc0d11301ee4617762d19cc24308
7
- data.tar.gz: 6a421ce03eda89cf332b767e76d2c89dbd5577ecc36dc08d4d696f3ee8e1f84a6160bfd4fdba32f6bd30d3aaacc62cb3f7d07e7c0b7363bce389a198455a7a8e
6
+ metadata.gz: 9151cd717f8bcae3f2c7552cf8660bb005a32406011c7c4e126f98cfe8e8971d7c21ee46e89fde9c4a39f156c1dac7dfecd636c84e44872343d72de8c0eae511
7
+ data.tar.gz: 570ced037808a33bf36843ebf378922a6bdb2e074781391831fe85bc1129c5b201e8114011ea5f511c3addaeb7e4f52e7cfc89ab4ef35271b494fb63ab7dbeae
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.4.0 (2026-09-16)
2
+
3
+ * Feature: [#47](https://github.com/savonrb/akami/pull/47) Support signed WS-Security timestamps. `Signature.new` accepts a `timestamp: true` option (plus optional `created_at`/`expires_at`) that adds a `wsu:Timestamp` to the Security header and covers it with its own digest Reference, so the timestamp cannot be altered without invalidating the signature.
4
+ * Feature: [#46](https://github.com/savonrb/akami/pull/46) Handle InclusiveNamespaces configuration when verifying signatures.
5
+ * Fix: [#62](https://github.com/savonrb/akami/pull/62) Fix signature verification for multi-token documents and signed attachments. The signer's certificate is now selected via the `SecurityTokenReference` instead of always taking the first `BinarySecurityToken`, and `cid:` attachment references can be verified against caller-supplied `decrypted_attachments`.
6
+
7
+ ## 1.3.3 (2024-02-13)
8
+
9
+ * Explicitly declare base64 dep in gemfile (useful for testing against ruby-head).
10
+
1
11
  ## 1.3.2 (2024-01-09)
2
12
 
3
13
  * Stop patching Hash [#35](https://github.com/savonrb/akami/pull/35)
@@ -28,3 +38,4 @@
28
38
  ## 1.0.0 (2011-07-03)
29
39
 
30
40
  * Initial version extracted from the [Savon](http://rubygems.org/gems/savon) library.
41
+
@@ -1,8 +1,8 @@
1
1
  module Akami
2
2
  module C14nHelper
3
- def canonicalize(xml)
3
+ def canonicalize(xml, inclusive_namespaces = nil)
4
4
  return unless xml
5
- xml.canonicalize Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
5
+ xml.canonicalize Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0, inclusive_namespaces
6
6
  end
7
7
  end
8
8
  end
@@ -3,9 +3,9 @@ module Akami
3
3
  # Returns a new Hash with +hash+ and +other_hash+ merged recursively.
4
4
  # Modifies +hash+ in place.
5
5
  def self.deep_merge!(hash, other_hash)
6
- other_hash.each_pair do |k,v|
6
+ other_hash.each_pair do |k, v|
7
7
  tv = hash[k]
8
- hash[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? deep_merge!(tv.dup, v) : v
8
+ hash[k] = (tv.is_a?(Hash) && v.is_a?(Hash)) ? deep_merge!(tv.dup, v) : v
9
9
  end
10
10
  hash
11
11
  end
data/lib/akami/version.rb CHANGED
@@ -1,5 +1,3 @@
1
1
  module Akami
2
-
3
- VERSION = '1.3.2'
4
-
2
+ VERSION = "1.4.0"
5
3
  end
@@ -2,10 +2,9 @@ module Akami
2
2
  class WSSE
3
3
  # Contains certs for WSSE::Signature
4
4
  class Certs
5
-
6
5
  def initialize(certs = {})
7
6
  certs.each do |key, value|
8
- self.send :"#{key}=", value
7
+ send :"#{key}=", value
9
8
  end
10
9
  end
11
10
 
@@ -12,6 +12,9 @@ module Akami
12
12
  # For a +Savon::WSSE::Certs+ object. To hold the certs we need to sign.
13
13
  attr_accessor :certs
14
14
 
15
+ # Wheater to sign the timestamp or not ant their time
16
+ attr_accessor :timestamp, :created_at, :expires_at
17
+
15
18
  # Without a document, the document cannot be signed.
16
19
  # Generate the document once, and then set document and recall #to_token
17
20
  def document
@@ -23,17 +26,20 @@ module Akami
23
26
  @document = Nokogiri::XML(document)
24
27
  end
25
28
 
26
- ExclusiveXMLCanonicalizationAlgorithm = 'http://www.w3.org/2001/10/xml-exc-c14n#'.freeze
27
- RSASHA1SignatureAlgorithm = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'.freeze
28
- SHA1DigestAlgorithm = 'http://www.w3.org/2000/09/xmldsig#sha1'.freeze
29
+ ExclusiveXMLCanonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#".freeze
30
+ RSASHA1SignatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1".freeze
31
+ SHA1DigestAlgorithm = "http://www.w3.org/2000/09/xmldsig#sha1".freeze
29
32
 
30
- X509v3ValueType = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3'.freeze
31
- Base64EncodingType = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'.freeze
33
+ X509v3ValueType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3".freeze
34
+ Base64EncodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary".freeze
32
35
 
33
- SignatureNamespace = 'http://www.w3.org/2000/09/xmldsig#'.freeze
36
+ SignatureNamespace = "http://www.w3.org/2000/09/xmldsig#".freeze
34
37
 
35
- def initialize(certs = Certs.new)
38
+ def initialize(certs = Certs.new, options = {})
36
39
  @certs = certs
40
+ @timestamp = options[:timestamp] || false
41
+ @created_at = options[:created_at]
42
+ @expires_at = options[:expires_at]
37
43
  end
38
44
 
39
45
  def have_document?
@@ -50,6 +56,26 @@ module Akami
50
56
  @body_id ||= "Body-#{uid}".freeze
51
57
  end
52
58
 
59
+ def timestamp_id
60
+ @timestamp_id ||= "TS-#{uid}".freeze
61
+ end
62
+
63
+ def wsu_timestamp_hash
64
+ return {} unless timestamp
65
+ {
66
+ "wsu:Timestamp" => {
67
+ "wsu:Created" => (@created_at ||= Time.now).utc.xmlschema,
68
+ "wsu:Expires" => (@expires_at ||= created_at + 60).utc.xmlschema
69
+ },
70
+ :attributes! => {
71
+ "wsu:Timestamp" => {
72
+ "wsu:Id" => timestamp_id,
73
+ "xmlns:wsu" => WSU_NAMESPACE
74
+ }
75
+ }
76
+ }
77
+ end
78
+
53
79
  def security_token_id
54
80
  @security_token_id ||= "SecurityToken-#{uid}".freeze
55
81
  end
@@ -57,7 +83,14 @@ module Akami
57
83
  def body_attributes
58
84
  {
59
85
  "xmlns:wsu" => Akami::WSSE::WSU_NAMESPACE,
60
- "wsu:Id" => body_id,
86
+ "wsu:Id" => body_id
87
+ }
88
+ end
89
+
90
+ def timestamp_attributes
91
+ {
92
+ "xmlns:wsu" => Akami::WSSE::WSU_NAMESPACE,
93
+ "wsu:Id" => timestamp_id
61
94
  }
62
95
  end
63
96
 
@@ -65,20 +98,20 @@ module Akami
65
98
  return {} unless have_document?
66
99
 
67
100
  sig = signed_info.merge(key_info).merge(signature_value)
68
- sig.merge! :order! => []
69
- [ "SignedInfo", "SignatureValue", "KeyInfo" ].each do |key|
101
+ sig[:order!] = []
102
+ ["SignedInfo", "SignatureValue", "KeyInfo"].each do |key|
70
103
  sig[:order!] << key if sig[key]
71
104
  end
72
105
 
73
106
  token = {
74
107
  "Signature" => sig,
75
- :attributes! => { "Signature" => { "xmlns" => SignatureNamespace } },
108
+ :attributes! => {"Signature" => {"xmlns" => SignatureNamespace}}
76
109
  }
77
110
 
78
111
  Akami::HashHelper.deep_merge!(token, binary_security_token) if certs.cert
79
112
 
80
- token.merge! :order! => []
81
- [ "wsse:BinarySecurityToken", "Signature" ].each do |key|
113
+ token[:order!] = []
114
+ ["wsse:BinarySecurityToken", "Signature"].each do |key|
82
115
  token[:order!] << key if token[key]
83
116
  end
84
117
 
@@ -89,13 +122,13 @@ module Akami
89
122
 
90
123
  def binary_security_token
91
124
  {
92
- "wsse:BinarySecurityToken" => Base64.encode64(certs.cert.to_der).gsub("\n", ''),
93
- :attributes! => { "wsse:BinarySecurityToken" => {
125
+ "wsse:BinarySecurityToken" => Base64.encode64(certs.cert.to_der).delete("\n"),
126
+ :attributes! => {"wsse:BinarySecurityToken" => {
94
127
  "wsu:Id" => security_token_id,
95
- 'EncodingType' => Base64EncodingType,
96
- 'ValueType' => X509v3ValueType,
97
- "xmlns:wsu" => Akami::WSSE::WSU_NAMESPACE,
98
- } }
128
+ "EncodingType" => Base64EncodingType,
129
+ "ValueType" => X509v3ValueType,
130
+ "xmlns:wsu" => Akami::WSSE::WSU_NAMESPACE
131
+ }}
99
132
  }
100
133
  end
101
134
 
@@ -104,18 +137,18 @@ module Akami
104
137
  "KeyInfo" => {
105
138
  "wsse:SecurityTokenReference" => {
106
139
  "wsse:Reference/" => nil,
107
- :attributes! => { "wsse:Reference/" => {
140
+ :attributes! => {"wsse:Reference/" => {
108
141
  "ValueType" => X509v3ValueType,
109
- "URI" => "##{security_token_id}",
110
- } }
142
+ "URI" => "##{security_token_id}"
143
+ }}
111
144
  },
112
- :attributes! => { "wsse:SecurityTokenReference" => { "xmlns:wsu" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" } },
113
- },
145
+ :attributes! => {"wsse:SecurityTokenReference" => {"xmlns:wsu" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"}}
146
+ }
114
147
  }
115
148
  end
116
149
 
117
150
  def signature_value
118
- { "SignatureValue" => the_signature }
151
+ {"SignatureValue" => the_signature}
119
152
  rescue MissingCertificate
120
153
  {}
121
154
  end
@@ -125,26 +158,35 @@ module Akami
125
158
  "SignedInfo" => {
126
159
  "CanonicalizationMethod/" => nil,
127
160
  "SignatureMethod/" => nil,
128
- "Reference" => [
129
- #signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => timestamp_digest }),
130
- signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => body_digest }),
131
- ],
161
+ "Reference" => references,
132
162
  :attributes! => {
133
- "CanonicalizationMethod/" => { "Algorithm" => ExclusiveXMLCanonicalizationAlgorithm },
134
- "SignatureMethod/" => { "Algorithm" => RSASHA1SignatureAlgorithm },
135
- "Reference" => { "URI" => ["##{body_id}"] },
163
+ "CanonicalizationMethod/" => {"Algorithm" => ExclusiveXMLCanonicalizationAlgorithm},
164
+ "SignatureMethod/" => {"Algorithm" => RSASHA1SignatureAlgorithm},
165
+ "Reference" => {"URI" => reference_uris}
136
166
  },
137
- :order! => [ "CanonicalizationMethod/", "SignatureMethod/", "Reference" ],
138
- },
167
+ :order! => ["CanonicalizationMethod/", "SignatureMethod/", "Reference"]
168
+ }
139
169
  }
140
170
  end
141
171
 
172
+ def references
173
+ ref = [signed_info_transforms.merge(signed_info_digest_method).merge({"DigestValue" => body_digest})]
174
+ ref << signed_info_transforms.merge(signed_info_digest_method).merge({"DigestValue" => timestamp_digest}) if timestamp
175
+ ref
176
+ end
177
+
178
+ def reference_uris
179
+ ref_uris = ["##{body_id}"]
180
+ ref_uris << "##{timestamp_id}" if timestamp
181
+ ref_uris
182
+ end
183
+
142
184
  def the_signature
143
185
  raise MissingCertificate, "Expected a private_key for signing" unless certs.private_key
144
186
  signed_info = at_xpath(@document, "//Envelope/Header/Security/Signature/SignedInfo")
145
187
  signed_info = signed_info ? canonicalize(signed_info) : ""
146
- signature = certs.private_key.sign(OpenSSL::Digest::SHA1.new, signed_info)
147
- Base64.encode64(signature).gsub("\n", '') # TODO: DRY calls to Base64.encode64(...).gsub("\n", '')
188
+ signature = certs.private_key.sign(OpenSSL::Digest.new("SHA1"), signed_info)
189
+ Base64.encode64(signature).delete("\n") # TODO: DRY calls to Base64.encode64(...).gsub("\n", '')
148
190
  end
149
191
 
150
192
  def body_digest
@@ -152,16 +194,23 @@ module Akami
152
194
  Base64.encode64(OpenSSL::Digest::SHA1.digest(body)).strip
153
195
  end
154
196
 
197
+ def timestamp_digest
198
+ return nil unless timestamp
199
+
200
+ canonical_timestamp = canonicalize(at_xpath(@document, "//Envelope/Header/Security/Timestamp"))
201
+ Base64.encode64(OpenSSL::Digest::SHA1.digest(canonical_timestamp)).strip if canonical_timestamp
202
+ end
203
+
155
204
  def signed_info_digest_method
156
- { "DigestMethod/" => nil, :attributes! => { "DigestMethod/" => { "Algorithm" => SHA1DigestAlgorithm } } }
205
+ {"DigestMethod/" => nil, :attributes! => {"DigestMethod/" => {"Algorithm" => SHA1DigestAlgorithm}}}
157
206
  end
158
207
 
159
208
  def signed_info_transforms
160
- { "Transforms" => { "Transform/" => nil, :attributes! => { "Transform/" => { "Algorithm" => ExclusiveXMLCanonicalizationAlgorithm } } } }
209
+ {"Transforms" => {"Transform/" => nil, :attributes! => {"Transform/" => {"Algorithm" => ExclusiveXMLCanonicalizationAlgorithm}}}}
161
210
  end
162
211
 
163
212
  def uid
164
- OpenSSL::Digest::SHA1.hexdigest([Time.now, rand].collect(&:to_s).join('/'))
213
+ OpenSSL::Digest::SHA1.hexdigest([Time.now, rand].join("/"))
165
214
  end
166
215
  end
167
216
  end
@@ -1,5 +1,5 @@
1
- require 'nokogiri'
2
- require 'openssl'
1
+ require "nokogiri"
2
+ require "openssl"
3
3
 
4
4
  module Akami
5
5
  class WSSE
@@ -11,19 +11,27 @@ module Akami
11
11
 
12
12
  class InvalidDigest < RuntimeError; end
13
13
  class InvalidSignedValue < RuntimeError; end
14
+ class MissingDecryptedAttachment < RuntimeError; end
14
15
 
15
16
  attr_reader :document
16
17
 
17
- def initialize(xml)
18
+ # @param xml [String] The XML document to verify
19
+ # @param decrypted_attachments [Hash] A hash of decrypted attachments: { 'id' => 'decrypted_string' }
20
+ # For example: the decrypted_attachments of a gzipped xml is the gzipped base64 string, the result of the decryption
21
+ # { 'phase4-att-1f34-4d68a..' => 'kZ\xB4\xCD}\xCB..' }
22
+ def initialize(xml, decrypted_attachments: {})
18
23
  @document = Nokogiri::XML(xml.to_s, &:noblanks)
24
+ @decrypted_attachments = decrypted_attachments
19
25
  end
20
26
 
21
27
  # Returns XML namespaces that are used internally for document querying.
22
28
  def namespaces
23
29
  @namespaces ||= {
24
30
  wse: Akami::WSSE::WSE_NAMESPACE,
25
- ds: 'http://www.w3.org/2000/09/xmldsig#',
31
+ wsse: Akami::WSSE::WSE_NAMESPACE,
32
+ ds: "http://www.w3.org/2000/09/xmldsig#",
26
33
  wsu: Akami::WSSE::WSU_NAMESPACE,
34
+ ec: Akami::WSSE::Signature::ExclusiveXMLCanonicalizationAlgorithm
27
35
  }
28
36
  end
29
37
 
@@ -32,21 +40,31 @@ module Akami
32
40
 
33
41
  # Returns signer's certificate, bundled in signed document
34
42
  def certificate
35
- certificate_value = document.at_xpath('//wse:Security/wse:BinarySecurityToken', namespaces).text.strip
36
- OpenSSL::X509::Certificate.new Base64.decode64(certificate_value)
43
+ binary_security_tokens = document.xpath("//wse:Security/wse:BinarySecurityToken", namespaces)
44
+ if binary_security_tokens.size > 1
45
+ signature_certificate_id = document.at_xpath(
46
+ "//wse:Security/ds:Signature/ds:KeyInfo/wsse:SecurityTokenReference/wsse:Reference",
47
+ namespaces
48
+ )["URI"][1..] # strip leading '#'
49
+ certificate_value = document.at_xpath("//wse:Security/wse:BinarySecurityToken[@wsu:Id=\"#{signature_certificate_id}\"]", namespaces)
50
+ else
51
+ certificate_value = binary_security_tokens.first
52
+ end
53
+
54
+ OpenSSL::X509::Certificate.new Base64.decode64(certificate_value.text.strip)
37
55
  end
38
56
 
39
57
  # Validates document signature, returns +true+ on success, +false+ otherwise.
40
58
  def valid?
41
59
  verify
42
- rescue InvalidDigest, InvalidSignedValue
43
- return false
60
+ rescue InvalidDigest, InvalidSignedValue, MissingDecryptedAttachment
61
+ false
44
62
  end
45
63
 
46
64
  # Validates document signature and digests and raises if anything mismatches.
47
65
  def verify!
48
66
  verify
49
- rescue InvalidDigest, InvalidSignedValue => e
67
+ rescue InvalidDigest, InvalidSignedValue, MissingDecryptedAttachment => e
50
68
  raise InvalidSignature, e.message
51
69
  end
52
70
 
@@ -58,55 +76,68 @@ module Akami
58
76
  #
59
77
  # digesters['http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'] = OpenSSL::Digest::SHA512.new
60
78
 
61
- def digesters
62
- @digesters
63
- end
79
+ attr_reader :digesters
64
80
 
65
81
  private
66
82
 
67
83
  def verify
68
- document.xpath('//wse:Security/ds:Signature/ds:SignedInfo/ds:Reference', namespaces).each do |ref|
69
- digest_algorithm = ref.at_xpath('//ds:DigestMethod', namespaces)['Algorithm']
70
- element_id = ref.attributes['URI'].value[1..-1] # strip leading '#'
71
- element = document.at_xpath(%(//*[@wsu:Id="#{element_id}"]), namespaces)
72
- unless supplied_digest(element) == generate_digest(element, digest_algorithm)
84
+ document.xpath("//wse:Security/ds:Signature/ds:SignedInfo/ds:Reference", namespaces).each do |ref|
85
+ digest_algorithm = ref.at_xpath("//ds:DigestMethod", namespaces)["Algorithm"]
86
+
87
+ transform_inclusive_ns = inclusive_namespaces(ref, ".//ds:Transforms/ds:Transform/ec:InclusiveNamespaces")
88
+
89
+ ref_uri = ref.attributes["URI"].value
90
+ if ref_uri.start_with?("#")
91
+ element_id = ref_uri.sub(/^#/, "")
92
+ element = document.at_xpath(%(//*[@wsu:Id="#{element_id}"]), namespaces)
93
+ generated_digest = generate_digest(element, digest_algorithm, transform_inclusive_ns)
94
+ else
95
+ element_id = ref_uri.sub(/^cid:/, "")
96
+ element = @decrypted_attachments[element_id]
97
+ raise MissingDecryptedAttachment, "Missing decrypted attachment for #{element_id}" if element.nil?
98
+ generated_digest = digest(element, digest_algorithm).strip
99
+ end
100
+
101
+ unless supplied_digest(ref) == generated_digest
73
102
  raise InvalidDigest, "Invalid Digest for #{element_id}"
74
103
  end
75
104
  end
76
105
 
77
- data = canonicalize(signed_info)
106
+ canonicalization_inclusive_ns = inclusive_namespaces(document, "//ds:CanonicalizationMethod/ec:InclusiveNamespaces")
107
+
108
+ data = canonicalize(signed_info, canonicalization_inclusive_ns)
78
109
  signature = Base64.decode64(signature_value)
79
- signature_algorithm = document.at_xpath('//wse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod', namespaces)['Algorithm']
110
+ signature_algorithm = document.at_xpath("//wse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod", namespaces)["Algorithm"]
80
111
  signature_digester = digester_for_signature_method(signature_algorithm)
81
112
 
82
113
  certificate.public_key.verify(signature_digester, signature, data) or raise InvalidSignedValue, "Could not verify the signature value"
83
114
  end
84
115
 
116
+ def inclusive_namespaces(ref, xpath)
117
+ inclusive_namespaces_element = ref.at_xpath(xpath, namespaces)
118
+ inclusive_namespaces_element["PrefixList"].split if inclusive_namespaces_element
119
+ end
120
+
85
121
  def signed_info
86
- document.at_xpath('//wse:Security/ds:Signature/ds:SignedInfo', namespaces)
122
+ document.at_xpath("//wse:Security/ds:Signature/ds:SignedInfo", namespaces)
87
123
  end
88
124
 
89
125
  # Generate digest for a given +element+ (or its XPath) with a given +algorithm+
90
- def generate_digest(element, algorithm)
126
+ def generate_digest(element, algorithm, inclusive_namespaces = nil)
91
127
  element = document.at_xpath(element, namespaces) if element.is_a? String
92
- xml = canonicalize(element)
128
+ xml = canonicalize(element, inclusive_namespaces)
93
129
  digest(xml, algorithm).strip
94
130
  end
95
131
 
96
132
  def supplied_digest(element)
97
- element = document.at_xpath(element, namespaces) if element.is_a? String
98
- find_digest_value element.attributes['Id'].value
133
+ element.at_xpath(".//ds:DigestValue", namespaces).text
99
134
  end
100
135
 
101
136
  def signature_value
102
- element = document.at_xpath('//wse:Security/ds:Signature/ds:SignatureValue', namespaces)
137
+ element = document.at_xpath("//wse:Security/ds:Signature/ds:SignatureValue", namespaces)
103
138
  element ? element.text : ""
104
139
  end
105
140
 
106
- def find_digest_value(id)
107
- document.at_xpath(%(//wse:Security/ds:Signature/ds:SignedInfo/ds:Reference[@URI="##{id}"]/ds:DigestValue), namespaces).text
108
- end
109
-
110
141
  # Calculate digest for string with given algorithm URL and Base64 encodes it.
111
142
  def digest(string, algorithm)
112
143
  Base64.encode64 digester(algorithm).digest(string)
@@ -115,8 +146,8 @@ module Akami
115
146
  # Returns digester for calculating digest for signature verification
116
147
  def digester_for_signature_method(algorithm_url)
117
148
  signature_digest_mapping = {
118
- 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' => 'http://www.w3.org/2000/09/xmldsig#sha1',
119
- 'http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411' => 'http://www.w3.org/2001/04/xmldsig-more#gostr3411',
149
+ "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => "http://www.w3.org/2000/09/xmldsig#sha1",
150
+ "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411" => "http://www.w3.org/2001/04/xmldsig-more#gostr3411"
120
151
  }
121
152
  digest_url = signature_digest_mapping[algorithm_url] || algorithm_url
122
153
  digester(digest_url)
@@ -124,23 +155,24 @@ module Akami
124
155
 
125
156
  # Constructors for known digest calculating objects
126
157
  DIGESTERS = {
127
- # SHA1
128
- 'http://www.w3.org/2000/09/xmldsig#sha1' => lambda { OpenSSL::Digest::SHA1.new },
129
- # SHA 256
130
- 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' => lambda { OpenSSL::Digest::SHA256.new },
131
- # GOST R 34.11-94
132
- # You need correctly configured gost engine in your system OpenSSL, requires OpenSSL >= 1.0.0
133
- # see https://github.com/openssl/openssl/blob/master/engines/ccgost/README.gost
134
- 'http://www.w3.org/2001/04/xmldsig-more#gostr3411' => lambda {
135
- if defined? JRUBY_VERSION
136
- OpenSSL::Digest.new('GOST3411')
137
- else
138
- OpenSSL::Engine.load
139
- gost_engine = OpenSSL::Engine.by_id('gost')
140
- gost_engine.set_default(0xFFFF)
141
- gost_engine.digest('md_gost94')
142
- end
143
- },
158
+ # SHA1
159
+ "http://www.w3.org/2000/09/xmldsig#sha1" => lambda { OpenSSL::Digest.new("SHA1") },
160
+ # SHA 256
161
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => lambda { OpenSSL::Digest.new("SHA256") },
162
+ "http://www.w3.org/2001/04/xmlenc#sha256" => lambda { OpenSSL::Digest.new("SHA256") },
163
+ # GOST R 34.11-94
164
+ # You need correctly configured gost engine in your system OpenSSL, requires OpenSSL >= 1.0.0
165
+ # see https://github.com/openssl/openssl/blob/master/engines/ccgost/README.gost
166
+ "http://www.w3.org/2001/04/xmldsig-more#gostr3411" => lambda {
167
+ if defined? JRUBY_VERSION
168
+ OpenSSL::Digest.new("GOST3411")
169
+ else
170
+ OpenSSL::Engine.load
171
+ gost_engine = OpenSSL::Engine.by_id("gost")
172
+ gost_engine.set_default(0xFFFF)
173
+ gost_engine.digest("md_gost94")
174
+ end
175
+ }
144
176
  }
145
177
 
146
178
  # Returns instance of +OpenSSL::Digest+ class, initialized, reset, and ready to calculate new hashes.
@@ -152,7 +184,6 @@ module Akami
152
184
  end
153
185
  @digesters[url].reset
154
186
  end
155
-
156
187
  end
157
188
  end
158
189
  end
data/lib/akami/wsse.rb CHANGED
@@ -9,12 +9,10 @@ require "akami/wsse/verify_signature"
9
9
  require "akami/wsse/signature"
10
10
 
11
11
  module Akami
12
-
13
12
  # = Akami::WSSE
14
13
  #
15
14
  # Building Web Service Security.
16
15
  class WSSE
17
-
18
16
  # Namespace for WS Security Secext.
19
17
  WSE_NAMESPACE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
20
18
 
@@ -90,24 +88,23 @@ module Akami
90
88
 
91
89
  # Returns the XML for a WSSE header.
92
90
  def to_xml
93
- if signature? and signature.have_document?
94
- Gyoku.xml wsse_signature.merge!(hash)
95
- elsif username_token? && timestamp?
96
- Gyoku.xml wsse_username_token.merge!(wsu_timestamp) {
97
- |key, v1, v2| v1.merge!(v2) {
98
- |key, v1, v2| v1.merge!(v2)
99
- }
100
- }
101
- elsif username_token?
102
- Gyoku.xml wsse_username_token.merge!(hash)
103
- elsif timestamp?
104
- Gyoku.xml wsu_timestamp.merge!(hash)
105
- else
106
- ""
107
- end
91
+ h = wsse_signature if signature? && signature.have_document?
92
+ h = merge_hashes_with_keys(h, wsu_timestamp) if timestamp?
93
+ h = merge_hashes_with_keys(h, wsse_username_token) if username_token?
94
+
95
+ return "" unless h
96
+ Gyoku.xml h
108
97
  end
109
98
 
110
- private
99
+ private
100
+
101
+ def merge_hashes_with_keys(hash_one, hash_two)
102
+ return hash_two unless hash_one
103
+ keys = hash_one["wsse:Security"][:order!] | hash_two["wsse:Security"][:order!]
104
+ Akami::HashHelper.deep_merge!(hash_one, hash_two)
105
+ hash_one["wsse:Security"][:order!] = keys
106
+ hash_one
107
+ end
111
108
 
112
109
  # Returns a Hash containing wsse:UsernameToken details.
113
110
  def wsse_username_token
@@ -117,14 +114,14 @@ module Akami
117
114
  "wsse:Nonce" => Base64.encode64(nonce).chomp,
118
115
  "wsu:Created" => timestamp,
119
116
  "wsse:Password" => digest_password,
120
- :attributes! => { "wsse:Password" => { "Type" => PASSWORD_DIGEST_URI }, "wsse:Nonce" => { "EncodingType" => BASE64_URI } }
117
+ :attributes! => {"wsse:Password" => {"Type" => PASSWORD_DIGEST_URI}, "wsse:Nonce" => {"EncodingType" => BASE64_URI}}
121
118
  # clear the nonce after each use
122
119
  @nonce = nil
123
120
  else
124
121
  token = security_hash :wsse, "UsernameToken",
125
122
  "wsse:Username" => username,
126
123
  "wsse:Password" => password,
127
- :attributes! => { "wsse:Password" => { "Type" => PASSWORD_TEXT_URI } }
124
+ :attributes! => {"wsse:Password" => {"Type" => PASSWORD_TEXT_URI}}
128
125
  end
129
126
  token
130
127
  end
@@ -135,7 +132,15 @@ module Akami
135
132
  # First key/value is tag/hash
136
133
  tag, hash = signature_hash.shift
137
134
 
138
- security_hash nil, tag, hash, signature_hash
135
+ sig = security_hash nil, tag, hash, signature_hash, true
136
+ if signature.timestamp
137
+ wsu_timestamp_hash = signature.wsu_timestamp_hash
138
+ sig["wsse:Security"]["wsu:Timestamp"] = wsu_timestamp_hash["wsu:Timestamp"] # attributes should be merged and not overridden
139
+ sig["wsse:Security"][:attributes!].merge!(wsu_timestamp_hash[:attributes!])
140
+ sig["wsse:Security"][:order!] << "wsu:Timestamp"
141
+ end
142
+
143
+ sig
139
144
  end
140
145
 
141
146
  # Returns a Hash containing wsu:Timestamp details.
@@ -147,24 +152,23 @@ module Akami
147
152
 
148
153
  # Returns a Hash containing wsse/wsu Security details for a given
149
154
  # +namespace+, +tag+ and +hash+.
150
- def security_hash(namespace, tag, hash, extra_info = {})
155
+ def security_hash(namespace, tag, hash, extra_info = {}, signature_request = false)
151
156
  key = [namespace, tag].compact.join(":")
152
157
 
153
158
  sec_hash = {
154
159
  "wsse:Security" => {
155
- key => hash
160
+ key => hash,
161
+ :order! => [key]
156
162
  },
157
- :attributes! => { "wsse:Security" => { "xmlns:wsse" => WSE_NAMESPACE } }
163
+ :attributes! => {"wsse:Security" => {"xmlns:wsse" => WSE_NAMESPACE}}
158
164
  }
159
165
 
160
- unless extra_info.empty?
161
- sec_hash["wsse:Security"].merge!(extra_info)
162
- end
166
+ sec_hash["wsse:Security"].merge!(extra_info) unless extra_info.empty?
163
167
 
164
- if signature?
165
- sec_hash[:attributes!].merge!("soapenv:mustUnderstand" => "1")
168
+ if signature_request
169
+ sec_hash[:attributes!]["soapenv:mustUnderstand"] = "1"
166
170
  else
167
- sec_hash["wsse:Security"].merge!(:attributes! => { key => { "wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSU_NAMESPACE } })
171
+ sec_hash["wsse:Security"][:attributes!] = {key => {"wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSU_NAMESPACE}}
168
172
  end
169
173
 
170
174
  sec_hash
@@ -201,6 +205,5 @@ module Akami
201
205
  def hash
202
206
  @hash ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
203
207
  end
204
-
205
208
  end
206
209
  end
@@ -11,7 +11,7 @@ module Akami
11
11
  end
12
12
 
13
13
  def local_name_xpath(xpath)
14
- xpath.gsub(%r{([/]*)([A-Za-z]+)([/]*)}) { "#{$1}*[local-name()='#{$2}']#{$3}" }
14
+ xpath.gsub(%r{(/*)([A-Za-z]+)(/*)}) { "#{$1}*[local-name()='#{$2}']#{$3}" }
15
15
  end
16
16
  end
17
17
  end
data/lib/akami.rb CHANGED
@@ -2,10 +2,8 @@ require "akami/version"
2
2
  require "akami/wsse"
3
3
 
4
4
  module Akami
5
-
6
5
  # Returns a new <tt>Akami::WSSE</tt>.
7
6
  def self.wsse
8
7
  WSSE.new
9
8
  end
10
-
11
9
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: akami
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Harrington
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: gyoku
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: rake
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -104,7 +117,6 @@ licenses:
104
117
  - MIT
105
118
  metadata:
106
119
  rubygems_mfa_required: 'true'
107
- post_install_message:
108
120
  rdoc_options: []
109
121
  require_paths:
110
122
  - lib
@@ -112,15 +124,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
124
  requirements:
113
125
  - - ">="
114
126
  - !ruby/object:Gem::Version
115
- version: 2.7.0
127
+ version: 3.0.0
116
128
  required_rubygems_version: !ruby/object:Gem::Requirement
117
129
  requirements:
118
130
  - - ">="
119
131
  - !ruby/object:Gem::Version
120
132
  version: '0'
121
133
  requirements: []
122
- rubygems_version: 3.4.10
123
- signing_key:
134
+ rubygems_version: 3.6.9
124
135
  specification_version: 4
125
136
  summary: Web Service Security
126
137
  test_files: []