ruby-saml 0.7.2 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- data/.gitignore +1 -0
- data/Gemfile +8 -6
- data/README.md +18 -2
- data/lib/onelogin/ruby-saml/authrequest.rb +8 -5
- data/lib/onelogin/ruby-saml/logoutresponse.rb +2 -2
- data/lib/onelogin/ruby-saml/response.rb +16 -10
- data/lib/onelogin/ruby-saml/settings.rb +5 -2
- data/lib/onelogin/ruby-saml/validation_error.rb +1 -1
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +5 -4
- data/ruby-saml.gemspec +1 -1
- data/test/certificates/r1_certificate2_base64 +1 -0
- data/test/logoutresponse_test.rb +1 -1
- data/test/request_test.rb +17 -0
- data/test/response_test.rb +23 -3
- data/test/responses/r1_response6.xml.base64 +1 -0
- data/test/settings_test.rb +4 -1
- data/test/test_helper.rb +9 -1
- data/test/xml_security_test.rb +29 -19
- metadata +77 -59
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -4,10 +4,12 @@ gemspec
|
|
4
4
|
|
5
5
|
group :test do
|
6
6
|
gem "ruby-debug", "~> 0.10.4", :require => nil, :platforms => :ruby_18
|
7
|
-
gem "debugger", "~> 1.1
|
8
|
-
gem "shoulda"
|
9
|
-
gem "rake"
|
10
|
-
gem "mocha"
|
11
|
-
gem "nokogiri"
|
12
|
-
gem "timecop"
|
7
|
+
gem "debugger", "~> 1.1", :require => nil, :platforms => :ruby_19
|
8
|
+
gem "shoulda", "~> 2.11"
|
9
|
+
gem "rake", "~> 10"
|
10
|
+
gem "mocha", "~> 0.14"
|
11
|
+
gem "nokogiri", "~> 1.5"
|
12
|
+
gem "timecop", "<= 0.6.0"
|
13
|
+
gem "systemu", "~> 2"
|
14
|
+
gem "rspec", "~> 2"
|
13
15
|
end
|
data/README.md
CHANGED
@@ -90,17 +90,19 @@ What's left at this point, is to wrap it all up in a controller and point the in
|
|
90
90
|
If are using saml:AttributeStatement to transfare metadata, like the user name, you can access all the attributes through response.attributes. It
|
91
91
|
contains all the saml:AttributeStatement with its 'Name' as a indifferent key and the one saml:AttributeValue as value.
|
92
92
|
|
93
|
+
```ruby
|
93
94
|
response = Onelogin::Saml::Response.new(params[:SAMLResponse])
|
94
95
|
response.settings = saml_settings
|
95
96
|
|
96
97
|
response.attributes[:username]
|
98
|
+
```
|
97
99
|
|
98
100
|
## Service Provider Metadata
|
99
101
|
|
100
102
|
To form a trusted pair relationship with the IdP, the SP (you) need to provide metadata XML
|
101
|
-
to the IdP for various good reasons. (Caching, certificate lookups,
|
103
|
+
to the IdP for various good reasons. (Caching, certificate lookups, relaying party permissions, etc)
|
102
104
|
|
103
|
-
The class Onelogin::Saml::
|
105
|
+
The class Onelogin::Saml::Metadata takes care of this by reading the Settings and returning XML. All
|
104
106
|
you have to do is add a controller to return the data, then give this URL to the IdP administrator.
|
105
107
|
The metdata will be polled by the IdP every few minutes, so updating your settings should propagate
|
106
108
|
to the IdP settings.
|
@@ -116,6 +118,20 @@ to the IdP settings.
|
|
116
118
|
end
|
117
119
|
```
|
118
120
|
|
121
|
+
## Clock Drift
|
122
|
+
|
123
|
+
Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition" then this may be due to clock differences between your system and that of the Identity Provider.
|
124
|
+
|
125
|
+
First, ensure that both systems synchronize their clocks, using for example the industry standard [Network Time Protocol (NTP)](http://en.wikipedia.org/wiki/Network_Time_Protocol).
|
126
|
+
|
127
|
+
Even then you may experience intermittent issues though, because the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift you can initialize the response passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
response = Onelogin::Saml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1)
|
131
|
+
```
|
132
|
+
|
133
|
+
Make sure to keep the value as comfortably small as possible to keep security risks to a minimum.
|
134
|
+
|
119
135
|
## Note on Patches/Pull Requests
|
120
136
|
|
121
137
|
* Fork the project.
|
@@ -13,6 +13,7 @@ module Onelogin
|
|
13
13
|
params = {} if params.nil?
|
14
14
|
|
15
15
|
request_doc = create_authentication_xml_doc(settings)
|
16
|
+
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
16
17
|
|
17
18
|
request = ""
|
18
19
|
request_doc.write(request)
|
@@ -35,7 +36,7 @@ module Onelogin
|
|
35
36
|
def create_authentication_xml_doc(settings)
|
36
37
|
uuid = "_" + UUID.new.generate
|
37
38
|
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
38
|
-
# Create AuthnRequest root element using REXML
|
39
|
+
# Create AuthnRequest root element using REXML
|
39
40
|
request_doc = REXML::Document.new
|
40
41
|
|
41
42
|
root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
|
@@ -43,6 +44,8 @@ module Onelogin
|
|
43
44
|
root.attributes['IssueInstant'] = time
|
44
45
|
root.attributes['Version'] = "2.0"
|
45
46
|
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
|
47
|
+
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
|
48
|
+
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
|
46
49
|
|
47
50
|
# Conditionally defined elements based on settings
|
48
51
|
if settings.assertion_consumer_service_url != nil
|
@@ -53,7 +56,7 @@ module Onelogin
|
|
53
56
|
issuer.text = settings.issuer
|
54
57
|
end
|
55
58
|
if settings.name_identifier_format != nil
|
56
|
-
root.add_element "samlp:NameIDPolicy", {
|
59
|
+
root.add_element "samlp:NameIDPolicy", {
|
57
60
|
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
58
61
|
# Might want to make AllowCreate a setting?
|
59
62
|
"AllowCreate" => "true",
|
@@ -62,14 +65,14 @@ module Onelogin
|
|
62
65
|
end
|
63
66
|
|
64
67
|
# BUG fix here -- if an authn_context is defined, add the tags with an "exact"
|
65
|
-
# match required for authentication to succeed. If this is not defined,
|
68
|
+
# match required for authentication to succeed. If this is not defined,
|
66
69
|
# the IdP will choose default rules for authentication. (Shibboleth IdP)
|
67
70
|
if settings.authn_context != nil
|
68
|
-
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
71
|
+
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
69
72
|
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
70
73
|
"Comparison" => "exact",
|
71
74
|
}
|
72
|
-
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
75
|
+
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
73
76
|
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
74
77
|
}
|
75
78
|
class_ref.text = settings.authn_context
|
@@ -105,7 +105,7 @@ module Onelogin
|
|
105
105
|
if soft
|
106
106
|
@schema.validate(@xml).map{ return false }
|
107
107
|
else
|
108
|
-
@schema.validate(@xml).map{ |error|
|
108
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
@@ -151,4 +151,4 @@ module Onelogin
|
|
151
151
|
end
|
152
152
|
end
|
153
153
|
end
|
154
|
-
end
|
154
|
+
end
|
@@ -78,7 +78,7 @@ module Onelogin
|
|
78
78
|
parse_time(node, "SessionNotOnOrAfter")
|
79
79
|
end
|
80
80
|
end
|
81
|
-
|
81
|
+
|
82
82
|
# Checks the status of the response for a "Success" code
|
83
83
|
def success?
|
84
84
|
@status_code ||= begin
|
@@ -92,6 +92,14 @@ module Onelogin
|
|
92
92
|
@conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
|
93
93
|
end
|
94
94
|
|
95
|
+
def not_before
|
96
|
+
@not_before ||= parse_time(conditions, "NotBefore")
|
97
|
+
end
|
98
|
+
|
99
|
+
def not_on_or_after
|
100
|
+
@not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
|
101
|
+
end
|
102
|
+
|
95
103
|
def issuer
|
96
104
|
@issuer ||= begin
|
97
105
|
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
@@ -110,7 +118,7 @@ module Onelogin
|
|
110
118
|
validate_structure(soft) &&
|
111
119
|
validate_response_state(soft) &&
|
112
120
|
validate_conditions(soft) &&
|
113
|
-
document.
|
121
|
+
document.validate_document(get_fingerprint, soft) &&
|
114
122
|
success?
|
115
123
|
end
|
116
124
|
|
@@ -161,16 +169,14 @@ module Onelogin
|
|
161
169
|
return true if conditions.nil?
|
162
170
|
return true if options[:skip_conditions]
|
163
171
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
172
|
+
now = Time.now.utc
|
173
|
+
|
174
|
+
if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
|
175
|
+
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
|
168
176
|
end
|
169
177
|
|
170
|
-
if
|
171
|
-
|
172
|
-
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
|
173
|
-
end
|
178
|
+
if not_on_or_after && now >= not_on_or_after
|
179
|
+
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
|
174
180
|
end
|
175
181
|
|
176
182
|
true
|
@@ -16,10 +16,13 @@ module Onelogin
|
|
16
16
|
attr_accessor :sessionindex
|
17
17
|
attr_accessor :assertion_consumer_logout_service_url
|
18
18
|
attr_accessor :compress_request
|
19
|
+
attr_accessor :double_quote_xml_attribute_values
|
20
|
+
attr_accessor :passive
|
21
|
+
attr_accessor :protocol_binding
|
19
22
|
|
20
23
|
private
|
21
|
-
|
22
|
-
DEFAULTS = {:compress_request => true}
|
24
|
+
|
25
|
+
DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false}
|
23
26
|
end
|
24
27
|
end
|
25
28
|
end
|
data/lib/xml_security.rb
CHANGED
@@ -44,9 +44,10 @@ module XMLSecurity
|
|
44
44
|
extract_signed_element_id
|
45
45
|
end
|
46
46
|
|
47
|
-
def
|
47
|
+
def validate_document(idp_cert_fingerprint, soft = true)
|
48
48
|
# get cert from response
|
49
49
|
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
|
50
|
+
raise Onelogin::Saml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
|
50
51
|
base64_cert = cert_element.text
|
51
52
|
cert_text = Base64.decode64(base64_cert)
|
52
53
|
cert = OpenSSL::X509::Certificate.new(cert_text)
|
@@ -58,10 +59,10 @@ module XMLSecurity
|
|
58
59
|
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Fingerprint mismatch"))
|
59
60
|
end
|
60
61
|
|
61
|
-
|
62
|
+
validate_signature(base64_cert, soft)
|
62
63
|
end
|
63
64
|
|
64
|
-
def
|
65
|
+
def validate_signature(base64_cert, soft = true)
|
65
66
|
# validate references
|
66
67
|
|
67
68
|
# check for inclusive namespaces
|
@@ -93,7 +94,7 @@ module XMLSecurity
|
|
93
94
|
|
94
95
|
hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
|
95
96
|
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
96
|
-
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
97
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
97
98
|
|
98
99
|
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
|
99
100
|
|
data/ruby-saml.gemspec
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
MIIEYTCCA0mgAwIBAgIJAMax+2BoUJmCMA0GCSqGSIb3DQEBBQUAMIHGMQswCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xIzAhBgNVBAoMGldlbGxzcHJpbmcgV29ybGR3aWRlLCBJbmMuMRwwGgYDVQQLDBNTeXN0ZW1zIEVuZ2luZWVyaW5nMSQwIgYDVQQDDBtzc28ud2VsbHNwcmluZ3dvcmxkd2lkZS5jb20xKTAnBgkqhkiG9w0BCQEWGml0QHdlbGxzcHJpbmd3b3JsZHdpZGUuY29tMB4XDTEzMDIyNzIzNTUwOFoXDTIzMDIyNzIzNTUwOFowgcYxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczEQMA4GA1UEBwwHQ2hpY2FnbzEjMCEGA1UECgwaV2VsbHNwcmluZyBXb3JsZHdpZGUsIEluYy4xHDAaBgNVBAsME1N5c3RlbXMgRW5naW5lZXJpbmcxJDAiBgNVBAMMG3Nzby53ZWxsc3ByaW5nd29ybGR3aWRlLmNvbTEpMCcGCSqGSIb3DQEJARYaaXRAd2VsbHNwcmluZ3dvcmxkd2lkZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNhrDaXa00JAIRaxEyDrK/Zjj8bBTQD5dPgugDndYf1AOpzSGpGFU+lPu0QRv0o66K64HrF24FATWI18Q6aZ+xX8QbuBrfia6hOFef29Sk5paS9+DcDCmisuNpl84kbbiazy6S6cFtcdrG9/cr2iXtYmIzz7EfUcP/UVAp24ZW7dWhcvxoqxF9n6Fj94N+rA0dmUFUGz6glm7us3p36xbkiUMpgr3feD/9P34H+2YFsQ2b2DblDI5Z7YULHxBsl5nuhPLFuPN1olcWQBsJYO6iHElFRH+487L2yZ1mLVXKI0LFb/w1rAJpPeUc8E5s1MATAjNx3wPwwqgw30sKtoXAgMBAAGjUDBOMB0GA1UdDgQWBBSrGGV9w3hGXTafkJLUaWBsWiDGaTAfBgNVHSMEGDAWgBSrGGV9w3hGXTafkJLUaWBsWiDGaTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCcvZV0VIwIRD4C5CItFwfNRF2LRBmYiNh4FJbo0wESbH0cWT9vXNdcjcx6PHrIj7ICErSCR5eZmIrSgLEBEkptjVsiFsHWSvMHv37WaHwyhZWnhutss32aP9+ifxQ1lzwm54jZWZsVVVFQH155BDsVeU1UwEhvcCExFa7RNjyvqQrZmyQwMFSzL1cQp0humu0hHLtAI7E32lp5itw6kTOfyhjB8d1bzBVZe6RY64RxOPEcx+9hkrHmfCohdt644jRtPdLTvqqxpscYGD+L2QOt1HpbGgAdcgUZeUHo/eosqpwDOoyuFepz7JzqMncxFN//NmjnFGVZdGR+6bTxKUKq
|
data/test/logoutresponse_test.rb
CHANGED
@@ -102,7 +102,7 @@ class RubySamlTest < Test::Unit::TestCase
|
|
102
102
|
should "raise error for invalid xml" do
|
103
103
|
logoutresponse = Onelogin::Saml::Logoutresponse.new(invalid_xml_response, settings)
|
104
104
|
|
105
|
-
assert_raises(
|
105
|
+
assert_raises(Onelogin::Saml::ValidationError) { logoutresponse.validate! }
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
data/test/request_test.rb
CHANGED
@@ -46,6 +46,23 @@ class RequestTest < Test::Unit::TestCase
|
|
46
46
|
assert_match /^<samlp:AuthnRequest/, decoded
|
47
47
|
end
|
48
48
|
|
49
|
+
should "create the SAMLRequest URL parameter with IsPassive" do
|
50
|
+
settings = Onelogin::Saml::Settings.new
|
51
|
+
settings.idp_sso_target_url = "http://example.com"
|
52
|
+
settings.passive = true
|
53
|
+
auth_url = Onelogin::Saml::Authrequest.new.create(settings)
|
54
|
+
assert auth_url =~ /^http:\/\/example\.com\?SAMLRequest=/
|
55
|
+
payload = CGI.unescape(auth_url.split("=").last)
|
56
|
+
decoded = Base64.decode64(payload)
|
57
|
+
|
58
|
+
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
59
|
+
inflated = zstream.inflate(decoded)
|
60
|
+
zstream.finish
|
61
|
+
zstream.close
|
62
|
+
|
63
|
+
assert_match /<samlp:AuthnRequest[^<]* IsPassive='true'/, inflated
|
64
|
+
end
|
65
|
+
|
49
66
|
should "accept extra parameters" do
|
50
67
|
settings = Onelogin::Saml::Settings.new
|
51
68
|
settings.idp_sso_target_url = "http://example.com"
|
data/test/response_test.rb
CHANGED
@@ -120,7 +120,7 @@ class RubySamlTest < Test::Unit::TestCase
|
|
120
120
|
settings = Onelogin::Saml::Settings.new
|
121
121
|
response.settings = settings
|
122
122
|
settings.idp_cert_fingerprint = "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA"
|
123
|
-
XMLSecurity::SignedDocument.any_instance.expects(:
|
123
|
+
XMLSecurity::SignedDocument.any_instance.expects(:validate_signature).returns(true)
|
124
124
|
assert response.validate!
|
125
125
|
end
|
126
126
|
|
@@ -133,6 +133,15 @@ class RubySamlTest < Test::Unit::TestCase
|
|
133
133
|
assert response.validate!
|
134
134
|
end
|
135
135
|
|
136
|
+
should "validate the digest" do
|
137
|
+
response = Onelogin::Saml::Response.new(r1_response_document_6)
|
138
|
+
response.stubs(:conditions).returns(nil)
|
139
|
+
settings = Onelogin::Saml::Settings.new
|
140
|
+
settings.idp_cert = Base64.decode64(r1_signature_2)
|
141
|
+
response.settings = settings
|
142
|
+
assert response.validate!
|
143
|
+
end
|
144
|
+
|
136
145
|
should "validate SAML 2.0 XML structure" do
|
137
146
|
resp_xml = Base64.decode64(response_document_4).gsub(/emailAddress/,'test')
|
138
147
|
response = Onelogin::Saml::Response.new(Base64.encode64(resp_xml))
|
@@ -175,6 +184,17 @@ class RubySamlTest < Test::Unit::TestCase
|
|
175
184
|
response = Onelogin::Saml::Response.new(response_document_5)
|
176
185
|
assert response.send(:validate_conditions, true)
|
177
186
|
end
|
187
|
+
|
188
|
+
should "optionally allow for clock drift" do
|
189
|
+
# The NotBefore condition in the document is 2011-06-14T18:21:01.516Z
|
190
|
+
Time.stubs(:now).returns(Time.parse("2011-06-14T18:21:01Z"))
|
191
|
+
response = Onelogin::Saml::Response.new(response_document_5, :allowed_clock_drift => 0.515)
|
192
|
+
assert !response.send(:validate_conditions, true)
|
193
|
+
|
194
|
+
Time.stubs(:now).returns(Time.parse("2011-06-14T18:21:01Z"))
|
195
|
+
response = Onelogin::Saml::Response.new(response_document_5, :allowed_clock_drift => 0.516)
|
196
|
+
assert response.send(:validate_conditions, true)
|
197
|
+
end
|
178
198
|
end
|
179
199
|
|
180
200
|
context "#attributes" do
|
@@ -220,13 +240,13 @@ class RubySamlTest < Test::Unit::TestCase
|
|
220
240
|
response = Onelogin::Saml::Response.new(response_document)
|
221
241
|
assert_equal "https://app.onelogin.com/saml/metadata/13590", response.issuer
|
222
242
|
end
|
223
|
-
|
243
|
+
|
224
244
|
should "return the issuer inside the response" do
|
225
245
|
response = Onelogin::Saml::Response.new(response_document_2)
|
226
246
|
assert_equal "wibble", response.issuer
|
227
247
|
end
|
228
248
|
end
|
229
|
-
|
249
|
+
|
230
250
|
context "#success" do
|
231
251
|
should "find a status code that says success" do
|
232
252
|
response = Onelogin::Saml::Response.new(response_document)
|
@@ -0,0 +1 @@
|
|
1
|
+
PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNmMxZmY2ZmIwNDhjODcyYjdjYmQxNDU2NjA3NGYxN2JlOGQ5YzcyODM3IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxMy0wMy0yNVQxNTozNjowMFoiIERlc3RpbmF0aW9uPSJodHRwczovL3JwbS5uZXdyZWxpYy5jb20vYWNjb3VudHMvMTM2ODU3L3Nzby9zYW1sL2ZpbmFsaXplIiBJblJlc3BvbnNlVG89Il85ZTFmMzVkMC03NzhmLTAxMzAtMWRhOS0wNDJiMmI0ZmQyNjUiPjxzYW1sOklzc3Vlcj5odHRwczovL3Nzby53ZWxsc3ByaW5nd29ybGR3aWRlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJfMDMwNTgzYjVkN2FhOWY4ODQzODg2NmZhNjE2NDBhMzdjMzVlNGZkNjQ3IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxMy0wMy0yNVQxNTozNjowMFoiPjxzYW1sOklzc3Vlcj5odHRwczovL3Nzby53ZWxsc3ByaW5nd29ybGR3aWRlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfMDMwNTgzYjVkN2FhOWY4ODQzODg2NmZhNjE2NDBhMzdjMzVlNGZkNjQ3Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT50bjc3ajFjMUQ0SjdPaTNrNVZVWlI2YjlzZTQ9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPldCQ2lWUUM2ZWFscGlzT3NYUUxOaGFIb3ptRy9CWGsxb3lFd2EyVnVBQ2FjTGlSMFplRk9ScXNzQW1tWkVPZXVlSXd1TEUxLzhPZ0t6OUZFYWZrREh6blh4c0x1dXg1N1pPS2R5cTJtUXllNS9XV2NPRVJtYVFHeE5EeERUVzdod1BVUDFLRzJNMTZvbTlzc3RmNTNTSTRHYkg0MzRnM3BwNGlQMjlLVis4b0t6V1h3VUtQb0FkUTFpT2pENWNITXhmNU9oQXRsQ1h6b0JtYTV3K3lOcmgvWFVBMmE2am1ubXV2cWdPS2VUU3VzWWlLYlVIVTAxOE15cVFrcmJaTXFndC9SMTY2RWxVU3RzcFlKc0tYalpYN1hLOUQ4ekdNNWZhQVErZ3BReW1BVEFrRE84MGsrVXZQdjhxWUhXemdvRCtSaDJFNjJpMzdkODZkUjNUNEdOUT09PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRVlUQ0NBMG1nQXdJQkFnSUpBTWF4KzJCb1VKbUNNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JSEdNUXN3Q1FZRFZRUUdFd0pWVXpFUk1BOEdBMVVFQ0F3SVNXeHNhVzV2YVhNeEVEQU9CZ05WQkFjTUIwTm9hV05oWjI4eEl6QWhCZ05WQkFvTUdsZGxiR3h6Y0hKcGJtY2dWMjl5YkdSM2FXUmxMQ0JKYm1NdU1Sd3dHZ1lEVlFRTERCTlRlWE4wWlcxeklFVnVaMmx1WldWeWFXNW5NU1F3SWdZRFZRUUREQnR6YzI4dWQyVnNiSE53Y21sdVozZHZjbXhrZDJsa1pTNWpiMjB4S1RBbkJna3Foa2lHOXcwQkNRRVdHbWwwUUhkbGJHeHpjSEpwYm1kM2IzSnNaSGRwWkdVdVkyOXRNQjRYRFRFek1ESXlOekl6TlRVd09Gb1hEVEl6TURJeU56SXpOVFV3T0Zvd2djWXhDekFKQmdOVkJBWVRBbFZUTVJFd0R3WURWUVFJREFoSmJHeHBibTlwY3pFUU1BNEdBMVVFQnd3SFEyaHBZMkZuYnpFak1DRUdBMVVFQ2d3YVYyVnNiSE53Y21sdVp5QlhiM0pzWkhkcFpHVXNJRWx1WXk0eEhEQWFCZ05WQkFzTUUxTjVjM1JsYlhNZ1JXNW5hVzVsWlhKcGJtY3hKREFpQmdOVkJBTU1HM056Ynk1M1pXeHNjM0J5YVc1bmQyOXliR1IzYVdSbExtTnZiVEVwTUNjR0NTcUdTSWIzRFFFSkFSWWFhWFJBZDJWc2JITndjbWx1WjNkdmNteGtkMmxrWlM1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDek5ockRhWGEwMEpBSVJheEV5RHJLL1pqajhiQlRRRDVkUGd1Z0RuZFlmMUFPcHpTR3BHRlUrbFB1MFFSdjBvNjZLNjRIckYyNEZBVFdJMThRNmFaK3hYOFFidUJyZmlhNmhPRmVmMjlTazVwYVM5K0RjRENtaXN1TnBsODRrYmJpYXp5NlM2Y0Z0Y2RyRzkvY3IyaVh0WW1Jeno3RWZVY1AvVVZBcDI0Wlc3ZFdoY3Z4b3F4RjluNkZqOTROK3JBMGRtVUZVR3o2Z2xtN3VzM3AzNnhia2lVTXBncjNmZUQvOVAzNEgrMllGc1EyYjJEYmxESTVaN1lVTEh4QnNsNW51aFBMRnVQTjFvbGNXUUJzSllPNmlIRWxGUkgrNDg3TDJ5WjFtTFZYS0kwTEZiL3cxckFKcFBlVWM4RTVzMU1BVEFqTngzd1B3d3FndzMwc0t0b1hBZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlNyR0dWOXczaEdYVGFma0pMVWFXQnNXaURHYVRBZkJnTlZIU01FR0RBV2dCU3JHR1Y5dzNoR1hUYWZrSkxVYVdCc1dpREdhVEFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQ2N2WlYwVkl3SVJENEM1Q0l0RndmTlJGMkxSQm1ZaU5oNEZKYm8wd0VTYkgwY1dUOXZYTmRjamN4NlBIcklqN0lDRXJTQ1I1ZVptSXJTZ0xFQkVrcHRqVnNpRnNIV1N2TUh2MzdXYUh3eWhaV25odXRzczMyYVA5K2lmeFExbHp3bTU0alpXWnNWVlZGUUgxNTVCRHNWZVUxVXdFaHZjQ0V4RmE3Uk5qeXZxUXJabXlRd01GU3pMMWNRcDBodW11MGhITHRBSTdFMzJscDVpdHc2a1RPZnloakI4ZDFiekJWWmU2Ulk2NFJ4T1BFY3grOWhrckhtZkNvaGR0NjQ0alJ0UGRMVHZxcXhwc2NZR0QrTDJRT3QxSHBiR2dBZGNnVVplVUhvL2Vvc3Fwd0RPb3l1RmVwejdKenFNbmN4Rk4vL05tam5GR1ZaZEdSKzZiVHhLVUtxPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJycG0ubmV3cmVsaWMuY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+ZTQwYzA4OTA3NDVjZTkyNTBhZDIyM2I1OTA5MGNjNmRjNWQxZjVhMTwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMy0wMy0yNVQxNTo0MTowMFoiIFJlY2lwaWVudD0iaHR0cHM6Ly9ycG0ubmV3cmVsaWMuY29tL2FjY291bnRzLzEzNjg1Ny9zc28vc2FtbC9maW5hbGl6ZSIgSW5SZXNwb25zZVRvPSJfOWUxZjM1ZDAtNzc4Zi0wMTMwLTFkYTktMDQyYjJiNGZkMjY1Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTMtMDMtMjVUMTU6MzU6MzBaIiBOb3RPbk9yQWZ0ZXI9IjIwMTMtMDMtMjVUMTU6NDE6MDBaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPnJwbS5uZXdyZWxpYy5jb208L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDEzLTAzLTI1VDE1OjIwOjEyWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxMy0wMy0yNVQyMzozNjowMFoiIFNlc3Npb25JbmRleD0iX2ZmNjc4NWJkNTUxYjQ2NzVjMGI2MGI3ZTI2YzFmYmI3Y2Y1MDljZTk5OSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iaG9tZURpcmVjdG9yeSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPi9ob21lL2t5bGUuYmFjenluc2tpPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImdpZE51bWJlciIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPjEwNDc8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkTnVtYmVyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+MTA0Nzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzc2hQdWJsaWNLZXkiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFEQVFBQkFBQUJBUUMrL3U1N2tqOWNRVXNXcW5EOVNYY0lNcCsyUDF3aFREa2FHZ2RPVUtySHpNWmxPMEdrTk9oZFZralBGeFJrcGZZWk1sMldZZE9OQkNiK1ZmY3pyd3J6NFNjOHIvaFFkQUdpRWJEYzJLRHZkT1dSYkRGZGlSVThYNy93aFppOE1ibUx1ZmN1cmlRQmZ3ekpET2NiY2c4S0ZvN25GR3JoNmwrWmVzZ2lZWC9CV2daYmp0NFR4aHhRWHpCWDJSQXBqd1lLVzlRRkV2UCtCOWRxdHErenFsUVdUU0xFSmZobTNLSVdXUUlEUWEwdS9CRUZOYnhMRVVjR3pwdGNQZnRqaktUSjNzZCtwb1pTTlhpUUtKODNxNXpYaDM2QUxYQjlFN3IyTkZTNHlFdHJ6Q1ZqejNqSXhtT0E0TFRGUnNTV2V0aW13TXNrL1VxeFBUaUR5ZUJNQ1hPdCBLeWxlIEJhY3p5bnNraSAmbHQ7a3lsZS5iYWN6eW5za2lAd2VsbHNwcmluZ3dvcmxkd2lkZS5jb20mZ3Q7PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImxvZ2luU2hlbGwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj4vYmluL3pzaDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzaGFkb3dMYXN0Q2hhbmdlIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+MTM2MzExOTAyODwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjIuNS40LjQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5CYWN6eW5za2k8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idXJuOm9pZDoyLjUuNC4wIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dG9wPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnBvc2l4QWNjb3VudDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5leHRlbnNpYmxlT2JqZWN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFjY291bnQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bGRhcFB1YmxpY0tleTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5reWxlLmJhY3p5bnNraUB3ZWxsc3ByaW5nd29ybGR3aWRlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjIuNS40LjQyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+S3lsZTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5reWxlLmJhY3p5bnNraTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjIuNS40LjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5LeWxlIEJhY3p5bnNraTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5JY2VkIFRlYTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjIuNS40LjM1IiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+e1NTSEF9RVB2V3V1bkMxU1p0Nk5OdHMveTdpcFRJcVVMVjZJVWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idXJuOm9pZDowLjkuMjM0Mi4xOTIwMDMwMC4xMDAuMS45IiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+Kjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2lkOjIuMTYuODQwLjEuMTEzNzMwLjMuMS4zOSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmVuPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
|
data/test/settings_test.rb
CHANGED
@@ -11,7 +11,8 @@ class SettingsTest < Test::Unit::TestCase
|
|
11
11
|
:assertion_consumer_service_url, :issuer, :sp_name_qualifier,
|
12
12
|
:idp_sso_target_url, :idp_cert_fingerprint, :name_identifier_format,
|
13
13
|
:idp_slo_target_url, :name_identifier_value, :sessionindex,
|
14
|
-
:assertion_consumer_logout_service_url
|
14
|
+
:assertion_consumer_logout_service_url,
|
15
|
+
:passive, :protocol_binding
|
15
16
|
]
|
16
17
|
|
17
18
|
accessors.each do |accessor|
|
@@ -31,6 +32,8 @@ class SettingsTest < Test::Unit::TestCase
|
|
31
32
|
:idp_slo_target_url => "http://sso.muda.no/slo",
|
32
33
|
:idp_cert_fingerprint => "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
|
33
34
|
:name_identifier_format => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
35
|
+
:passive => true,
|
36
|
+
:protocol_binding => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
34
37
|
}
|
35
38
|
@settings = Onelogin::Saml::Settings.new(config)
|
36
39
|
|
data/test/test_helper.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'shoulda'
|
4
|
-
require 'mocha'
|
5
4
|
require 'ruby-debug'
|
5
|
+
require 'mocha/setup'
|
6
6
|
require 'timecop'
|
7
7
|
|
8
8
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
@@ -41,6 +41,10 @@ class Test::Unit::TestCase
|
|
41
41
|
@response_document5 ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'response5.xml.base64'))
|
42
42
|
end
|
43
43
|
|
44
|
+
def r1_response_document_6
|
45
|
+
@response_document6 ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'r1_response6.xml.base64'))
|
46
|
+
end
|
47
|
+
|
44
48
|
def ampersands_response
|
45
49
|
@ampersands_resposne ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'response_with_ampersands.xml.base64'))
|
46
50
|
end
|
@@ -64,4 +68,8 @@ class Test::Unit::TestCase
|
|
64
68
|
@signature1 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'certificate1'))
|
65
69
|
end
|
66
70
|
|
71
|
+
def r1_signature_2
|
72
|
+
@signature2 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'r1_certificate2_base64'))
|
73
|
+
end
|
74
|
+
|
67
75
|
end
|
data/test/xml_security_test.rb
CHANGED
@@ -11,31 +11,31 @@ class XmlSecurityTest < Test::Unit::TestCase
|
|
11
11
|
end
|
12
12
|
|
13
13
|
should "should run validate without throwing NS related exceptions" do
|
14
|
-
assert !@document.
|
14
|
+
assert !@document.validate_signature(@base64cert, true)
|
15
15
|
end
|
16
16
|
|
17
17
|
should "should run validate with throwing NS related exceptions" do
|
18
18
|
assert_raise(Onelogin::Saml::ValidationError) do
|
19
|
-
@document.
|
19
|
+
@document.validate_signature(@base64cert, false)
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
should "not raise an error when softly validating the document multiple times" do
|
24
24
|
assert_nothing_raised do
|
25
|
-
2.times { @document.
|
25
|
+
2.times { @document.validate_signature(@base64cert, true) }
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
should "should raise Fingerprint mismatch" do
|
30
30
|
exception = assert_raise(Onelogin::Saml::ValidationError) do
|
31
|
-
@document.
|
31
|
+
@document.validate_document("no:fi:ng:er:pr:in:t", false)
|
32
32
|
end
|
33
33
|
assert_equal("Fingerprint mismatch", exception.message)
|
34
34
|
end
|
35
35
|
|
36
36
|
should "should raise Digest mismatch" do
|
37
37
|
exception = assert_raise(Onelogin::Saml::ValidationError) do
|
38
|
-
@document.
|
38
|
+
@document.validate_signature(@base64cert, false)
|
39
39
|
end
|
40
40
|
assert_equal("Digest mismatch", exception.message)
|
41
41
|
end
|
@@ -47,50 +47,60 @@ class XmlSecurityTest < Test::Unit::TestCase
|
|
47
47
|
document = XMLSecurity::SignedDocument.new(response)
|
48
48
|
base64cert = document.elements["//ds:X509Certificate"].text
|
49
49
|
exception = assert_raise(Onelogin::Saml::ValidationError) do
|
50
|
-
document.
|
50
|
+
document.validate_signature(base64cert, false)
|
51
51
|
end
|
52
52
|
assert_equal("Key validation error", exception.message)
|
53
53
|
end
|
54
|
+
|
55
|
+
should "raise validation error when the X509Certificate is missing" do
|
56
|
+
response = Base64.decode64(response_document)
|
57
|
+
response.sub!(/<ds:X509Certificate>.*<\/ds:X509Certificate>/, "")
|
58
|
+
document = XMLSecurity::SignedDocument.new(response)
|
59
|
+
exception = assert_raise(Onelogin::Saml::ValidationError) do
|
60
|
+
document.validate_document("a fingerprint", false) # The fingerprint isn't relevant to this test
|
61
|
+
end
|
62
|
+
assert_equal("Certificate element missing in response (ds:X509Certificate)", exception.message)
|
63
|
+
end
|
54
64
|
end
|
55
65
|
|
56
66
|
context "Algorithms" do
|
57
67
|
should "validate using SHA1" do
|
58
68
|
@document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha1, false))
|
59
|
-
assert @document.
|
69
|
+
assert @document.validate_document("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")
|
60
70
|
end
|
61
71
|
|
62
72
|
should "validate using SHA256" do
|
63
73
|
@document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha256, false))
|
64
|
-
assert @document.
|
74
|
+
assert @document.validate_document("28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA")
|
65
75
|
end
|
66
76
|
|
67
77
|
should "validate using SHA384" do
|
68
78
|
@document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha384, false))
|
69
|
-
assert @document.
|
79
|
+
assert @document.validate_document("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")
|
70
80
|
end
|
71
81
|
|
72
82
|
should "validate using SHA512" do
|
73
83
|
@document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha512, false))
|
74
|
-
assert @document.
|
84
|
+
assert @document.validate_document("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")
|
75
85
|
end
|
76
86
|
end
|
77
|
-
|
87
|
+
|
78
88
|
context "XmlSecurity::SignedDocument" do
|
79
|
-
|
89
|
+
|
80
90
|
context "#extract_inclusive_namespaces" do
|
81
91
|
should "support explicit namespace resolution for exclusive canonicalization" do
|
82
92
|
response = fixture(:open_saml_response, false)
|
83
93
|
document = XMLSecurity::SignedDocument.new(response)
|
84
94
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
85
|
-
|
95
|
+
|
86
96
|
assert_equal %w[ xs ], inclusive_namespaces
|
87
97
|
end
|
88
|
-
|
98
|
+
|
89
99
|
should "support implicit namespace resolution for exclusive canonicalization" do
|
90
100
|
response = fixture(:no_signature_ns, false)
|
91
101
|
document = XMLSecurity::SignedDocument.new(response)
|
92
102
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
93
|
-
|
103
|
+
|
94
104
|
assert_equal %w[ #default saml ds xs xsi ], inclusive_namespaces
|
95
105
|
end
|
96
106
|
|
@@ -110,10 +120,10 @@ class XmlSecurityTest < Test::Unit::TestCase
|
|
110
120
|
should "return an empty list when inclusive namespace element is missing" do
|
111
121
|
response = fixture(:no_signature_ns, false)
|
112
122
|
response.slice! %r{<InclusiveNamespaces xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="#default saml ds xs xsi"/>}
|
113
|
-
|
123
|
+
|
114
124
|
document = XMLSecurity::SignedDocument.new(response)
|
115
125
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
116
|
-
|
126
|
+
|
117
127
|
assert inclusive_namespaces.empty?
|
118
128
|
end
|
119
129
|
end
|
@@ -146,5 +156,5 @@ class XmlSecurityTest < Test::Unit::TestCase
|
|
146
156
|
end
|
147
157
|
|
148
158
|
end
|
149
|
-
|
159
|
+
|
150
160
|
end
|
metadata
CHANGED
@@ -1,72 +1,79 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-saml
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 7
|
9
|
+
- 3
|
10
|
+
version: 0.7.3
|
6
11
|
platform: ruby
|
7
|
-
authors:
|
12
|
+
authors:
|
8
13
|
- OneLogin LLC
|
9
14
|
autorequire:
|
10
15
|
bindir: bin
|
11
16
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
|
18
|
+
date: 2014-02-20 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
15
21
|
name: canonix
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - '='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: 0.1.1
|
22
|
-
type: :runtime
|
23
22
|
prerelease: false
|
24
|
-
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
24
|
none: false
|
26
|
-
requirements:
|
27
|
-
- -
|
28
|
-
- !ruby/object:Gem::Version
|
25
|
+
requirements:
|
26
|
+
- - "="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 25
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 1
|
32
|
+
- 1
|
29
33
|
version: 0.1.1
|
30
|
-
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
31
37
|
name: uuid
|
32
|
-
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
33
40
|
none: false
|
34
|
-
requirements:
|
41
|
+
requirements:
|
35
42
|
- - ~>
|
36
|
-
- !ruby/object:Gem::Version
|
37
|
-
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 5
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 3
|
48
|
+
version: "2.3"
|
38
49
|
type: :runtime
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: nokogiri
|
39
53
|
prerelease: false
|
40
|
-
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
55
|
none: false
|
42
|
-
requirements:
|
56
|
+
requirements:
|
43
57
|
- - ~>
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
- - ! '>='
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 5
|
63
|
+
- 0
|
64
|
+
version: 1.5.0
|
54
65
|
type: :runtime
|
55
|
-
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ! '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
66
|
+
version_requirements: *id003
|
62
67
|
description: SAML toolkit for Ruby on Rails
|
63
68
|
email: support@onelogin.com
|
64
69
|
executables: []
|
70
|
+
|
65
71
|
extensions: []
|
66
|
-
|
72
|
+
|
73
|
+
extra_rdoc_files:
|
67
74
|
- LICENSE
|
68
75
|
- README.md
|
69
|
-
files:
|
76
|
+
files:
|
70
77
|
- .document
|
71
78
|
- .gitignore
|
72
79
|
- .travis.yml
|
@@ -91,6 +98,7 @@ files:
|
|
91
98
|
- lib/xml_security.rb
|
92
99
|
- ruby-saml.gemspec
|
93
100
|
- test/certificates/certificate1
|
101
|
+
- test/certificates/r1_certificate2_base64
|
94
102
|
- test/logoutrequest_test.rb
|
95
103
|
- test/logoutresponse_test.rb
|
96
104
|
- test/request_test.rb
|
@@ -102,6 +110,7 @@ files:
|
|
102
110
|
- test/responses/logoutresponse_fixtures.rb
|
103
111
|
- test/responses/no_signature_ns.xml
|
104
112
|
- test/responses/open_saml_response.xml
|
113
|
+
- test/responses/r1_response6.xml.base64
|
105
114
|
- test/responses/response1.xml.base64
|
106
115
|
- test/responses/response2.xml.base64
|
107
116
|
- test/responses/response3.xml.base64
|
@@ -117,31 +126,40 @@ files:
|
|
117
126
|
- test/xml_security_test.rb
|
118
127
|
homepage: http://github.com/onelogin/ruby-saml
|
119
128
|
licenses: []
|
129
|
+
|
120
130
|
post_install_message:
|
121
|
-
rdoc_options:
|
131
|
+
rdoc_options:
|
122
132
|
- --charset=UTF-8
|
123
|
-
require_paths:
|
133
|
+
require_paths:
|
124
134
|
- lib
|
125
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
136
|
none: false
|
127
|
-
requirements:
|
128
|
-
- -
|
129
|
-
- !ruby/object:Gem::Version
|
130
|
-
|
131
|
-
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
hash: 3
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
145
|
none: false
|
133
|
-
requirements:
|
134
|
-
- -
|
135
|
-
- !ruby/object:Gem::Version
|
136
|
-
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 3
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
version: "0"
|
137
153
|
requirements: []
|
154
|
+
|
138
155
|
rubyforge_project: http://www.rubygems.org/gems/ruby-saml
|
139
|
-
rubygems_version: 1.8.
|
156
|
+
rubygems_version: 1.8.25
|
140
157
|
signing_key:
|
141
158
|
specification_version: 3
|
142
159
|
summary: SAML Ruby Tookit
|
143
|
-
test_files:
|
160
|
+
test_files:
|
144
161
|
- test/certificates/certificate1
|
162
|
+
- test/certificates/r1_certificate2_base64
|
145
163
|
- test/logoutrequest_test.rb
|
146
164
|
- test/logoutresponse_test.rb
|
147
165
|
- test/request_test.rb
|
@@ -153,6 +171,7 @@ test_files:
|
|
153
171
|
- test/responses/logoutresponse_fixtures.rb
|
154
172
|
- test/responses/no_signature_ns.xml
|
155
173
|
- test/responses/open_saml_response.xml
|
174
|
+
- test/responses/r1_response6.xml.base64
|
156
175
|
- test/responses/response1.xml.base64
|
157
176
|
- test/responses/response2.xml.base64
|
158
177
|
- test/responses/response3.xml.base64
|
@@ -166,4 +185,3 @@ test_files:
|
|
166
185
|
- test/settings_test.rb
|
167
186
|
- test/test_helper.rb
|
168
187
|
- test/xml_security_test.rb
|
169
|
-
has_rdoc:
|