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 +4 -4
- data/CHANGELOG.md +11 -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 +34 -31
- data/lib/akami/xpath_helper.rb +1 -1
- data/lib/akami.rb +0 -2
- metadata +18 -7
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,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
|
+
|
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
|
|
|
@@ -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?
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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! => {
|
|
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! => {
|
|
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! => {
|
|
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
|
|
165
|
-
sec_hash[:attributes!]
|
|
168
|
+
if signature_request
|
|
169
|
+
sec_hash[:attributes!]["soapenv:mustUnderstand"] = "1"
|
|
166
170
|
else
|
|
167
|
-
sec_hash["wsse:Security"]
|
|
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
|
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
|
|
@@ -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:
|
|
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.
|
|
123
|
-
signing_key:
|
|
134
|
+
rubygems_version: 3.6.9
|
|
124
135
|
specification_version: 4
|
|
125
136
|
summary: Web Service Security
|
|
126
137
|
test_files: []
|