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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/akami/c14n_helper.rb +2 -2
- data/lib/akami/hash_helper.rb +2 -2
- data/lib/akami/version.rb +1 -3
- data/lib/akami/wsse/certs.rb +1 -2
- data/lib/akami/wsse/signature.rb +88 -39
- data/lib/akami/wsse/verify_signature.rb +80 -49
- data/lib/akami/wsse.rb +17 -11
- data/lib/akami/xpath_helper.rb +1 -1
- data/lib/akami.rb +0 -2
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 184a02f0bc62b95abd62b455fcd57dd5ffdf8c7d88a89b0651f8edb566fab1db
|
|
4
|
+
data.tar.gz: 39adab6b78b3a9346ce22b9bc3981e13f3bc67637fbc75276a48d6e0b0f9d0c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
|
data/lib/akami/c14n_helper.rb
CHANGED
|
@@ -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
|
data/lib/akami/hash_helper.rb
CHANGED
|
@@ -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
data/lib/akami/wsse/certs.rb
CHANGED
data/lib/akami/wsse/signature.rb
CHANGED
|
@@ -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 =
|
|
27
|
-
RSASHA1SignatureAlgorithm =
|
|
28
|
-
SHA1DigestAlgorithm =
|
|
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 =
|
|
31
|
-
Base64EncodingType =
|
|
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 =
|
|
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
|
|
69
|
-
[
|
|
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! => {
|
|
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
|
|
81
|
-
[
|
|
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).
|
|
93
|
-
:attributes! => {
|
|
125
|
+
"wsse:BinarySecurityToken" => Base64.encode64(certs.cert.to_der).delete("\n"),
|
|
126
|
+
:attributes! => {"wsse:BinarySecurityToken" => {
|
|
94
127
|
"wsu:Id" => security_token_id,
|
|
95
|
-
|
|
96
|
-
|
|
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! => {
|
|
140
|
+
:attributes! => {"wsse:Reference/" => {
|
|
108
141
|
"ValueType" => X509v3ValueType,
|
|
109
|
-
"URI" => "##{security_token_id}"
|
|
110
|
-
}
|
|
142
|
+
"URI" => "##{security_token_id}"
|
|
143
|
+
}}
|
|
111
144
|
},
|
|
112
|
-
:attributes! => {
|
|
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
|
-
{
|
|
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/" => {
|
|
134
|
-
"SignatureMethod/" => {
|
|
135
|
-
"Reference" => {
|
|
163
|
+
"CanonicalizationMethod/" => {"Algorithm" => ExclusiveXMLCanonicalizationAlgorithm},
|
|
164
|
+
"SignatureMethod/" => {"Algorithm" => RSASHA1SignatureAlgorithm},
|
|
165
|
+
"Reference" => {"URI" => reference_uris}
|
|
136
166
|
},
|
|
137
|
-
:order! => [
|
|
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
|
|
147
|
-
Base64.encode64(signature).
|
|
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
|
-
{
|
|
205
|
+
{"DigestMethod/" => nil, :attributes! => {"DigestMethod/" => {"Algorithm" => SHA1DigestAlgorithm}}}
|
|
157
206
|
end
|
|
158
207
|
|
|
159
208
|
def signed_info_transforms
|
|
160
|
-
{
|
|
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].
|
|
213
|
+
OpenSSL::Digest::SHA1.hexdigest([Time.now, rand].join("/"))
|
|
165
214
|
end
|
|
166
215
|
end
|
|
167
216
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
@digesters
|
|
63
|
-
end
|
|
79
|
+
attr_reader :digesters
|
|
64
80
|
|
|
65
81
|
private
|
|
66
82
|
|
|
67
83
|
def verify
|
|
68
|
-
document.xpath(
|
|
69
|
-
digest_algorithm = ref.at_xpath(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
95
|
+
return "" unless h
|
|
98
96
|
Gyoku.xml h
|
|
99
97
|
end
|
|
100
98
|
|
|
101
|
-
|
|
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! => {
|
|
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! => {
|
|
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! => {
|
|
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!]
|
|
169
|
+
sec_hash[:attributes!]["soapenv:mustUnderstand"] = "1"
|
|
164
170
|
else
|
|
165
|
-
sec_hash["wsse:Security"]
|
|
171
|
+
sec_hash["wsse:Security"][:attributes!] = {key => {"wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSU_NAMESPACE}}
|
|
166
172
|
end
|
|
167
173
|
|
|
168
174
|
sec_hash
|
data/lib/akami/xpath_helper.rb
CHANGED
data/lib/akami.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
137
|
-
signing_key:
|
|
134
|
+
rubygems_version: 3.6.9
|
|
138
135
|
specification_version: 4
|
|
139
136
|
summary: Web Service Security
|
|
140
137
|
test_files: []
|