akami 1.3.3 → 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: c1103cb0692c129241fbfd8360ab1dcfc96157c9ad7f2a28987ea3848ec06ab2
4
- data.tar.gz: d2e9cfc6c384c697be541aceda1bb3a4dc5e9654db159eca3fbc795dfc5d2671
3
+ metadata.gz: 184a02f0bc62b95abd62b455fcd57dd5ffdf8c7d88a89b0651f8edb566fab1db
4
+ data.tar.gz: 39adab6b78b3a9346ce22b9bc3981e13f3bc67637fbc75276a48d6e0b0f9d0c9
5
5
  SHA512:
6
- metadata.gz: c3bd5f7aa12bc379c4434dc880a23f9897a722ae2dd09352fb50a0bf807e932abc26d2703dfd2bcea463216fb746d499ce2eb80cff978b1feeb23404749a0253
7
- data.tar.gz: 84f9de77fef2521fa98df2c52f5e32b71f72b909da82f77a586e3aa746f8883610983b712c7ba11ff7833c409e72d243762358dfd6a46eead757ec9938b607e7
6
+ metadata.gz: 9151cd717f8bcae3f2c7552cf8660bb005a32406011c7c4e126f98cfe8e8971d7c21ee46e89fde9c4a39f156c1dac7dfecd636c84e44872343d72de8c0eae511
7
+ data.tar.gz: 570ced037808a33bf36843ebf378922a6bdb2e074781391831fe85bc1129c5b201e8114011ea5f511c3addaeb7e4f52e7cfc89ab4ef35271b494fb63ab7dbeae
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## 1.3.3 (2024-02-13)
2
8
 
3
9
  * Explicitly declare base64 dep in gemfile (useful for testing against ruby-head).
@@ -32,3 +38,4 @@
32
38
  ## 1.0.0 (2011-07-03)
33
39
 
34
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.3'
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
 
@@ -94,11 +92,11 @@ module Akami
94
92
  h = merge_hashes_with_keys(h, wsu_timestamp) if timestamp?
95
93
  h = merge_hashes_with_keys(h, wsse_username_token) if username_token?
96
94
 
97
- return '' unless h
95
+ return "" unless h
98
96
  Gyoku.xml h
99
97
  end
100
98
 
101
- private
99
+ private
102
100
 
103
101
  def merge_hashes_with_keys(hash_one, hash_two)
104
102
  return hash_two unless hash_one
@@ -116,14 +114,14 @@ module Akami
116
114
  "wsse:Nonce" => Base64.encode64(nonce).chomp,
117
115
  "wsu:Created" => timestamp,
118
116
  "wsse:Password" => digest_password,
119
- :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}}
120
118
  # clear the nonce after each use
121
119
  @nonce = nil
122
120
  else
123
121
  token = security_hash :wsse, "UsernameToken",
124
122
  "wsse:Username" => username,
125
123
  "wsse:Password" => password,
126
- :attributes! => { "wsse:Password" => { "Type" => PASSWORD_TEXT_URI } }
124
+ :attributes! => {"wsse:Password" => {"Type" => PASSWORD_TEXT_URI}}
127
125
  end
128
126
  token
129
127
  end
@@ -134,7 +132,15 @@ module Akami
134
132
  # First key/value is tag/hash
135
133
  tag, hash = signature_hash.shift
136
134
 
137
- security_hash nil, tag, hash, signature_hash, true
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
138
144
  end
139
145
 
140
146
  # Returns a Hash containing wsu:Timestamp details.
@@ -146,7 +152,7 @@ module Akami
146
152
 
147
153
  # Returns a Hash containing wsse/wsu Security details for a given
148
154
  # +namespace+, +tag+ and +hash+.
149
- def security_hash(namespace, tag, hash, extra_info = {}, signature_request=false)
155
+ def security_hash(namespace, tag, hash, extra_info = {}, signature_request = false)
150
156
  key = [namespace, tag].compact.join(":")
151
157
 
152
158
  sec_hash = {
@@ -154,15 +160,15 @@ module Akami
154
160
  key => hash,
155
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
166
  sec_hash["wsse:Security"].merge!(extra_info) unless extra_info.empty?
161
167
 
162
168
  if signature_request
163
- sec_hash[:attributes!].merge!("soapenv:mustUnderstand" => "1")
169
+ sec_hash[:attributes!]["soapenv:mustUnderstand"] = "1"
164
170
  else
165
- 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}}
166
172
  end
167
173
 
168
174
  sec_hash
@@ -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.3
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-02-13 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
@@ -118,7 +117,6 @@ licenses:
118
117
  - MIT
119
118
  metadata:
120
119
  rubygems_mfa_required: 'true'
121
- post_install_message:
122
120
  rdoc_options: []
123
121
  require_paths:
124
122
  - lib
@@ -133,8 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
131
  - !ruby/object:Gem::Version
134
132
  version: '0'
135
133
  requirements: []
136
- rubygems_version: 3.4.10
137
- signing_key:
134
+ rubygems_version: 3.6.9
138
135
  specification_version: 4
139
136
  summary: Web Service Security
140
137
  test_files: []