saml2 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/saml2/assertion.rb +10 -0
- data/lib/saml2/attribute.rb +32 -0
- data/lib/saml2/attribute/x500.rb +11 -2
- data/lib/saml2/base.rb +54 -0
- data/lib/saml2/conditions.rb +42 -27
- data/lib/saml2/entity.rb +23 -3
- data/lib/saml2/key.rb +2 -0
- data/lib/saml2/message.rb +15 -2
- data/lib/saml2/response.rb +150 -2
- data/lib/saml2/role.rb +11 -0
- data/lib/saml2/signable.rb +36 -8
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/certificate.pem +0 -1
- data/spec/fixtures/external-uri-reference-response.xml +48 -0
- data/spec/fixtures/othercertificate.pem +25 -0
- data/spec/fixtures/response_tampered_certificate.xml +25 -0
- data/spec/fixtures/response_tampered_signature.xml +46 -0
- data/spec/fixtures/response_with_encrypted_assertion.xml +58 -0
- data/spec/fixtures/test3-response.xml +9 -0
- data/spec/fixtures/test6-response.xml +10 -0
- data/spec/fixtures/test7-response.xml +10 -0
- data/spec/fixtures/xml_missigned_assertion.xml +84 -0
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +11 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +45 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +44 -0
- data/spec/fixtures/xslt-transform-response.xml +57 -0
- data/spec/lib/attribute_spec.rb +17 -0
- data/spec/lib/conditions_spec.rb +2 -2
- data/spec/lib/response_spec.rb +210 -1
- metadata +28 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af55cbf920b9c47ceee2e7f2fcad2878c042d1665f02b6933004ce7675cfb05f
|
4
|
+
data.tar.gz: 1f0f3e3cb1e009b7bf4d2af5e46a63d94d0d9d676d197554156cbc48c31786b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d1cac3ac228678cf72214505d7eebec1af9c33b5f9464163119dd666549d82b4ab8fed696f739469ed740d51f1c69cef6ac858e9cfca5633633df396494e0bc
|
7
|
+
data.tar.gz: f23498a3981fd055ba7b791e33efe78d5bec213fd91866d7cc863c84133858459f224d1a391394d2c33d655cea8e335ef4cd06a7ecf76a407dc93025573e3ca4
|
data/lib/saml2/assertion.rb
CHANGED
@@ -31,6 +31,16 @@ module SAML2
|
|
31
31
|
@conditions ||= Conditions.from_xml(xml.at_xpath('saml:Conditions', Namespaces::ALL))
|
32
32
|
end
|
33
33
|
|
34
|
+
# @return [Array<AuthnStatement]
|
35
|
+
def authn_statements
|
36
|
+
statements.select { |s| s.is_a?(AuthnStatement) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Array<AttributeStatement>]
|
40
|
+
def attribute_statements
|
41
|
+
statements.select { |s| s.is_a?(AttributeStatement) }
|
42
|
+
end
|
43
|
+
|
34
44
|
# @return [Array<AuthnStatement, AttributeStatement>]
|
35
45
|
def statements
|
36
46
|
@statements ||= load_object_array(xml, 'saml:AuthnStatement|saml:AttributeStatement')
|
data/lib/saml2/attribute.rb
CHANGED
@@ -170,6 +170,38 @@ module SAML2
|
|
170
170
|
end
|
171
171
|
end
|
172
172
|
|
173
|
+
# Convert the {AttributeStatement} to a {Hash}
|
174
|
+
#
|
175
|
+
# Repeated attributes become an array.
|
176
|
+
#
|
177
|
+
# @param name optional [:name, :friendly_name]
|
178
|
+
# Which name field to use as keys to the hash
|
179
|
+
def to_h(name = :friendly_name)
|
180
|
+
result = {}
|
181
|
+
attributes.each do |attribute|
|
182
|
+
key = attribute.send(name)
|
183
|
+
# fall back to name on missing friendly name;
|
184
|
+
# no need for the opposite, because name is required
|
185
|
+
key ||= attribute.name if name == :friendly_name
|
186
|
+
|
187
|
+
prior_value = result[key]
|
188
|
+
result[key] = if prior_value
|
189
|
+
value = Array.wrap(prior_value)
|
190
|
+
# repeated key; convert to array
|
191
|
+
if attribute.value.is_a?(Array)
|
192
|
+
# both values are arrays; concatenate them
|
193
|
+
prior_value.concat(attribute.value)
|
194
|
+
else
|
195
|
+
value << attribute.value
|
196
|
+
end
|
197
|
+
value
|
198
|
+
else
|
199
|
+
attribute.value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
result
|
203
|
+
end
|
204
|
+
|
173
205
|
def build(builder)
|
174
206
|
builder['saml'].AttributeStatement('xmlns:xs' => Namespaces::XS,
|
175
207
|
'xmlns:xsi' => Namespaces::XSI) do |statement|
|
data/lib/saml2/attribute/x500.rb
CHANGED
@@ -19,12 +19,12 @@ module SAML2
|
|
19
19
|
ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'
|
20
20
|
NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'
|
21
21
|
ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'
|
22
|
+
ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'
|
22
23
|
PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'
|
23
24
|
PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'
|
24
25
|
PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'
|
25
26
|
SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'
|
26
27
|
TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'
|
27
|
-
UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'
|
28
28
|
end
|
29
29
|
# http://www.ietf.org/rfc/rfc4519.txt
|
30
30
|
UID = USERID = 'urn:oid:0.9.2342.19200300.100.1.1'
|
@@ -35,7 +35,8 @@ module SAML2
|
|
35
35
|
# @param name_or_node [String, Nokogiri::XML::Element]
|
36
36
|
def self.recognizes?(name_or_node)
|
37
37
|
if name_or_node.is_a?(Nokogiri::XML::Element)
|
38
|
-
!!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL)
|
38
|
+
!!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL) ||
|
39
|
+
name_or_node['NameFormat'] == NameFormats::URI && OIDS.include?(name_or_node['Name'])
|
39
40
|
else
|
40
41
|
FRIENDLY_NAMES.include?(name_or_node) || OIDS.include?(name_or_node)
|
41
42
|
end
|
@@ -63,6 +64,14 @@ module SAML2
|
|
63
64
|
super(name, value, friendly_name, NameFormats::URI)
|
64
65
|
end
|
65
66
|
|
67
|
+
# (see Base.from_xml)
|
68
|
+
def from_xml(node)
|
69
|
+
super
|
70
|
+
# infer the friendly name if not provided
|
71
|
+
self.friendly_name ||= OIDS[name]
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
66
75
|
# (see Base#build)
|
67
76
|
def build(builder)
|
68
77
|
super
|
data/lib/saml2/base.rb
CHANGED
@@ -85,6 +85,56 @@ module SAML2
|
|
85
85
|
def build(builder)
|
86
86
|
end
|
87
87
|
|
88
|
+
# Decrypt (in-place) encrypted portions of this object
|
89
|
+
#
|
90
|
+
# Either the +keys+ parameter, or a block that returns key(s), should be
|
91
|
+
# provided.
|
92
|
+
#
|
93
|
+
# @param keys optional [Array<OpenSSL::PKey, String>, OpenSSL::PKey, String, nil]
|
94
|
+
# @yield Optional block to fetch the necessary keys, given information
|
95
|
+
# contained in the encrypted elements of which certificates it was
|
96
|
+
# encrypted for.
|
97
|
+
# @yieldparam allowed_certs [Array<OpenSSL::X509::Certificate, Hash, String, nil>]
|
98
|
+
# An array of certificates describing who the node was encrypted for.
|
99
|
+
# Identified by an X.509 Certificate, a hash with +:issuer+ and +:serial+
|
100
|
+
# keys, or a string of SubjectName.
|
101
|
+
# @yieldreturn [Array<OpenSSL::PKey, String>, OpenSSL::PKey, String, nil]
|
102
|
+
# @return [Boolean] If any nodes were present.
|
103
|
+
def decrypt(keys = nil)
|
104
|
+
encrypted_nodes = self.encrypted_nodes
|
105
|
+
encrypted_nodes.each do |node|
|
106
|
+
this_nodes_keys = keys
|
107
|
+
if keys.nil?
|
108
|
+
allowed_certs = node.xpath("dsig:KeyInfo/xenc:EncryptedKey/dsig:KeyInfo/dsig:X509Data", SAML2::Namespaces::ALL).map do |x509data|
|
109
|
+
if (cert = x509data.at_xpath("dsig:X509Certificate", SAML2::Namespaces::ALL)&.content&.strip)
|
110
|
+
OpenSSL::X509::Certificate.new(Base64.decode64(cert))
|
111
|
+
elsif (issuer_serial = x509data.at_xpath("dsig:X509IssuerSerial", SAML2::Namespaces::ALL))
|
112
|
+
{
|
113
|
+
issuer: issuer_serial.at_xpath("dsig:X509IssuerName", SAML2::Namespaces::ALL).content.strip,
|
114
|
+
serial: issuer_serial.at_xpath("dsig:X509SerialNumber", SAML2::Namespaces::ALL).content.strip.to_i,
|
115
|
+
}
|
116
|
+
elsif (subject_name = x509data.at_xpath("dsig:X509SubjectName", SAML2::Namespaces::ALL)&.content&.strip)
|
117
|
+
subject_name
|
118
|
+
end
|
119
|
+
end
|
120
|
+
this_nodes_keys = yield allowed_certs
|
121
|
+
end
|
122
|
+
this_nodes_keys = Array(this_nodes_keys)
|
123
|
+
raise ArgumentError("no decryption key provided or found") if this_nodes_keys.empty?
|
124
|
+
|
125
|
+
old_node = node.parent
|
126
|
+
this_nodes_keys.each_with_index do |key, i|
|
127
|
+
begin
|
128
|
+
old_node.replace(node.decrypt_with(key: key))
|
129
|
+
rescue XMLSec::DecryptionError
|
130
|
+
# swallow errors on all but the last key
|
131
|
+
raise if i - 1 == this_nodes_keys.length
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
!encrypted_nodes.empty?
|
136
|
+
end
|
137
|
+
|
88
138
|
def self.load_string_array(node, element)
|
89
139
|
node.xpath(element, Namespaces::ALL).map do |element_node|
|
90
140
|
element_node.content&.strip
|
@@ -119,6 +169,10 @@ module SAML2
|
|
119
169
|
self.class.load_object_array(node, element, klass)
|
120
170
|
end
|
121
171
|
|
172
|
+
def encrypted_nodes
|
173
|
+
xml.xpath('//xenc:EncryptedData', Namespaces::ALL)
|
174
|
+
end
|
175
|
+
|
122
176
|
def self.split_qname(qname)
|
123
177
|
if qname.include?(':')
|
124
178
|
qname.split(':', 2)
|
data/lib/saml2/conditions.rb
CHANGED
@@ -22,38 +22,46 @@ module SAML2
|
|
22
22
|
@not_before = Time.parse(node['NotBefore']) if node['NotBefore']
|
23
23
|
@not_on_or_after = Time.parse(node['NotOnOrAfter']) if node['NotOnOrAfter']
|
24
24
|
|
25
|
-
replace(node.
|
25
|
+
replace(node.element_children.map do |restriction|
|
26
|
+
klass = if self.class.const_defined?(restriction.name, false)
|
27
|
+
self.class.const_get(restriction.name, false)
|
28
|
+
else
|
29
|
+
Condition
|
30
|
+
end
|
31
|
+
klass.from_xml(restriction)
|
32
|
+
end)
|
26
33
|
end
|
27
34
|
|
28
35
|
# Evaluate these conditions.
|
29
36
|
#
|
30
|
-
# @param
|
37
|
+
# @param verification_time optional [Time]
|
31
38
|
# @param options
|
32
39
|
# Additional options to pass to specific {Condition}s
|
33
|
-
# @return [
|
40
|
+
# @return [Array<>]
|
34
41
|
# It's only valid if every sub-condition is completely valid.
|
35
42
|
# If any sub-condition is invalid, the whole statement is invalid.
|
36
43
|
# If the validity can't be determined due to an unsupported condition,
|
37
44
|
# +nil+ will be returned (which is false-ish)
|
38
|
-
def
|
39
|
-
options[:
|
40
|
-
|
41
|
-
|
45
|
+
def validate(verification_time: Time.now.utc, **options)
|
46
|
+
options[:verification_time] ||= verification_time
|
47
|
+
errors = []
|
48
|
+
if not_before && verification_time < not_before
|
49
|
+
errors << "not_before #{not_before} is later than now (#{verification_time})"
|
50
|
+
end
|
51
|
+
if not_on_or_after && verification_time >= not_on_or_after
|
52
|
+
errors << "not_on_or_after #{not_on_or_after} is earlier than now (#{verification_time})"
|
53
|
+
end
|
42
54
|
|
43
|
-
result = true
|
44
55
|
each do |condition|
|
45
|
-
|
46
|
-
case this_result
|
47
|
-
when false
|
48
|
-
return false
|
49
|
-
when nil
|
50
|
-
result = nil
|
51
|
-
when true
|
52
|
-
else
|
53
|
-
raise "unknown validity of #{condition}"
|
54
|
-
end
|
56
|
+
errors.concat(condition.validate(**options))
|
55
57
|
end
|
56
|
-
|
58
|
+
errors
|
59
|
+
end
|
60
|
+
|
61
|
+
# Use validate instead.
|
62
|
+
# @deprecated
|
63
|
+
def valid?(now: Time.now.utc, **options)
|
64
|
+
validate(verification_time: now, **options).empty?
|
57
65
|
end
|
58
66
|
|
59
67
|
# (see Base#build)
|
@@ -70,9 +78,13 @@ module SAML2
|
|
70
78
|
|
71
79
|
# Any unknown condition
|
72
80
|
class Condition < Base
|
73
|
-
# @return [
|
74
|
-
def
|
75
|
-
|
81
|
+
# @return []
|
82
|
+
def validate(_)
|
83
|
+
["unable to validate #{xml&.name || 'unrecognized'} condition"]
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid?(*args)
|
87
|
+
validate(*args).empty?
|
76
88
|
end
|
77
89
|
end
|
78
90
|
|
@@ -96,8 +108,11 @@ module SAML2
|
|
96
108
|
end
|
97
109
|
|
98
110
|
# @param audience [String]
|
99
|
-
def
|
100
|
-
Array.wrap(self.audience).include?(audience)
|
111
|
+
def validate(audience: nil, **_)
|
112
|
+
unless Array.wrap(self.audience).include?(audience)
|
113
|
+
return ["audience #{audience} not in allowed list of #{Array.wrap(self.audience).join(', ')}"]
|
114
|
+
end
|
115
|
+
[]
|
101
116
|
end
|
102
117
|
|
103
118
|
# (see Base#build)
|
@@ -113,9 +128,9 @@ module SAML2
|
|
113
128
|
class OneTimeUse < Condition
|
114
129
|
# The caller will need to see if this condition exists, and validate it
|
115
130
|
# using their own state store.
|
116
|
-
# @return [
|
117
|
-
def
|
118
|
-
|
131
|
+
# @return [[]]
|
132
|
+
def validate(_)
|
133
|
+
[]
|
119
134
|
end
|
120
135
|
|
121
136
|
# (see Base#build)
|
data/lib/saml2/entity.rb
CHANGED
@@ -82,10 +82,11 @@ module SAML2
|
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
|
86
|
-
|
85
|
+
# @param id [String] The Entity ID
|
86
|
+
def initialize(entity_id = nil)
|
87
|
+
super()
|
87
88
|
@valid_until = nil
|
88
|
-
@entity_id =
|
89
|
+
@entity_id = entity_id
|
89
90
|
@roles = []
|
90
91
|
@id = "_#{SecureRandom.uuid}"
|
91
92
|
end
|
@@ -152,5 +153,24 @@ module SAML2
|
|
152
153
|
super
|
153
154
|
end
|
154
155
|
end
|
156
|
+
|
157
|
+
# Validate a message is a valid response.
|
158
|
+
#
|
159
|
+
# @param message [Message]
|
160
|
+
# @param identity_provider [Entity]
|
161
|
+
def valid_response?(message,
|
162
|
+
identity_provider,
|
163
|
+
verification_time: Time.now.utc,
|
164
|
+
allow_expired_certificate: false)
|
165
|
+
unless message.is_a?(Response)
|
166
|
+
message.errors << "not a Response object"
|
167
|
+
return false
|
168
|
+
end
|
169
|
+
|
170
|
+
message.validate(service_provider: self,
|
171
|
+
identity_provider: identity_provider,
|
172
|
+
verification_time: verification_time,
|
173
|
+
allow_expired_certificate: allow_expired_certificate).empty?
|
174
|
+
end
|
155
175
|
end
|
156
176
|
end
|
data/lib/saml2/key.rb
CHANGED
data/lib/saml2/message.rb
CHANGED
@@ -44,6 +44,7 @@ module SAML2
|
|
44
44
|
class Message < Base
|
45
45
|
include Signable
|
46
46
|
|
47
|
+
attr_reader :errors
|
47
48
|
attr_writer :issuer, :destination
|
48
49
|
|
49
50
|
class << self
|
@@ -93,6 +94,7 @@ module SAML2
|
|
93
94
|
|
94
95
|
def initialize
|
95
96
|
super
|
97
|
+
@errors = []
|
96
98
|
@id = "_#{SecureRandom.uuid}"
|
97
99
|
@issue_instant = Time.now.utc
|
98
100
|
end
|
@@ -104,6 +106,11 @@ module SAML2
|
|
104
106
|
@issue_instant = nil
|
105
107
|
end
|
106
108
|
|
109
|
+
def validate
|
110
|
+
@errors = Schemas.protocol.validate(xml.document)
|
111
|
+
errors
|
112
|
+
end
|
113
|
+
|
107
114
|
# If the XML is valid according to SAML XSDs.
|
108
115
|
# @return [Boolean]
|
109
116
|
def valid_schema?
|
@@ -115,9 +122,15 @@ module SAML2
|
|
115
122
|
# (see Signable#validate_signature)
|
116
123
|
# @param verification_time
|
117
124
|
# Ignored. The message's {issue_instant} is always used.
|
118
|
-
def validate_signature(fingerprint: nil,
|
125
|
+
def validate_signature(fingerprint: nil,
|
126
|
+
cert: nil,
|
127
|
+
verification_time: issue_instant,
|
128
|
+
allow_expired_certificate: false)
|
119
129
|
# verify the signature (certificate's validity) as of the time the message was generated
|
120
|
-
super(fingerprint: fingerprint,
|
130
|
+
super(fingerprint: fingerprint,
|
131
|
+
cert: cert,
|
132
|
+
verification_time: verification_time,
|
133
|
+
allow_expired_certificate: allow_expired_certificate)
|
121
134
|
end
|
122
135
|
|
123
136
|
# (see Signable#sign)
|
data/lib/saml2/response.rb
CHANGED
@@ -77,6 +77,150 @@ module SAML2
|
|
77
77
|
remove_instance_variable(:@assertions)
|
78
78
|
end
|
79
79
|
|
80
|
+
# Validates a response is well-formed, signed, and optionally decrypts it
|
81
|
+
#
|
82
|
+
# @param service_provider [Entity]
|
83
|
+
# The metadata object for the {ServiceProvider} receiving this
|
84
|
+
# {Response}. The first {ServiceProvider} in the {Entity} is used.
|
85
|
+
# @param identity_provider [Entity]
|
86
|
+
# The metadata object for the {IdentityProvider} the {Response} is
|
87
|
+
# being received from. The first {IdentityProvider} in the {Entity} is
|
88
|
+
# used.
|
89
|
+
# @param verification_time optional [DateTime]
|
90
|
+
# Validate timestamps (signing certificate validity, issued at, etc.) as of
|
91
|
+
# this point in time.
|
92
|
+
def validate(service_provider:,
|
93
|
+
identity_provider:,
|
94
|
+
verification_time: Time.now.utc,
|
95
|
+
allow_expired_certificate: false)
|
96
|
+
raise ArgumentError, "service_provider should be an Entity object" unless service_provider.is_a?(Entity)
|
97
|
+
raise ArgumentError, "service_provider should have at least one service_provider role" unless (sp = service_provider.service_providers.first)
|
98
|
+
|
99
|
+
# validate the schema
|
100
|
+
super()
|
101
|
+
return errors unless errors.empty?
|
102
|
+
|
103
|
+
# not finding the issuer is not exceptional
|
104
|
+
if identity_provider.nil?
|
105
|
+
errors << "could not find issuer of response"
|
106
|
+
return errors
|
107
|
+
end
|
108
|
+
|
109
|
+
# getting the wrong data type is exceptional, and we should raise an error
|
110
|
+
raise ArgumentError, "identity_provider should be an Entity object" unless identity_provider.is_a?(Entity)
|
111
|
+
raise ArgumentError, "identity_provider should have at least one identity_provider role" unless (idp = identity_provider.identity_providers.first)
|
112
|
+
|
113
|
+
unless identity_provider.entity_id == issuer.id
|
114
|
+
errors << "received unexpected message from '#{issuer.id}'; expected it to be from '#{identity_provider.entity_id}'"
|
115
|
+
return errors
|
116
|
+
end
|
117
|
+
|
118
|
+
certificates = idp.signing_keys.map(&:certificate)
|
119
|
+
if idp.fingerprints.empty? && certificates.empty?
|
120
|
+
errors << "could not find certificate to validate message"
|
121
|
+
return errors
|
122
|
+
end
|
123
|
+
|
124
|
+
if signed?
|
125
|
+
unless (signature_errors = validate_signature(fingerprint: idp.fingerprints,
|
126
|
+
cert: certificates,
|
127
|
+
allow_expired_certificate: allow_expired_certificate)).empty?
|
128
|
+
return errors.concat(signature_errors)
|
129
|
+
end
|
130
|
+
response_signed = true
|
131
|
+
end
|
132
|
+
|
133
|
+
find_decryption_key = ->(embedded_certificates) do
|
134
|
+
key = nil
|
135
|
+
embedded_certificates.each do |cert_info|
|
136
|
+
cert = case cert_info
|
137
|
+
when OpenSSL::X509::Certificate; cert_info
|
138
|
+
when Hash; sp.encryption_keys.map(&:certificate).find { |c| c.serial == cert_info[:serial] }
|
139
|
+
end
|
140
|
+
next unless cert
|
141
|
+
key = sp.private_keys.find { |k| cert.check_private_key(k) }
|
142
|
+
break if key
|
143
|
+
end
|
144
|
+
if !key
|
145
|
+
# couldn't figure out which key to use; just try them all
|
146
|
+
next sp.private_keys
|
147
|
+
end
|
148
|
+
key
|
149
|
+
end
|
150
|
+
|
151
|
+
unless sp.private_keys.empty?
|
152
|
+
begin
|
153
|
+
decypted_anything = decrypt(&find_decryption_key)
|
154
|
+
rescue XMLSec::DecryptionError
|
155
|
+
errors << "unable to decrypt response"
|
156
|
+
return errors
|
157
|
+
end
|
158
|
+
|
159
|
+
if decypted_anything
|
160
|
+
# have to re-validate the schema, since we just replaced content
|
161
|
+
super()
|
162
|
+
return errors unless errors.empty?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
unless status.success?
|
167
|
+
errors << "response is not successful: #{status}"
|
168
|
+
return errors
|
169
|
+
end
|
170
|
+
|
171
|
+
assertion = assertions.first
|
172
|
+
unless assertion
|
173
|
+
errors << "no assertion found"
|
174
|
+
return errors
|
175
|
+
end
|
176
|
+
|
177
|
+
if assertion.signed?
|
178
|
+
unless (signature_errors = assertion.validate_signature(fingerprint: idp.fingerprints,
|
179
|
+
cert: certificates,
|
180
|
+
allow_expired_certificate: allow_expired_certificate)).empty?
|
181
|
+
return errors.concat(signature_errors)
|
182
|
+
end
|
183
|
+
assertion_signed = true
|
184
|
+
end
|
185
|
+
|
186
|
+
# only do our own issue instant validation if the assertion
|
187
|
+
# doesn't mandate any
|
188
|
+
unless assertion.conditions.not_on_or_after
|
189
|
+
if assertion.issue_instant + 5 * 60 < verification_time ||
|
190
|
+
assertion.issue_instant - 5 * 60 > verification_time
|
191
|
+
errors << "assertion not recently issued"
|
192
|
+
return errors
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
unless (condition_errors = assertion.conditions.validate(verification_time: verification_time,
|
197
|
+
audience: service_provider.entity_id)).empty?
|
198
|
+
return errors.concat(condition_errors)
|
199
|
+
end
|
200
|
+
|
201
|
+
if !response_signed && !assertion_signed
|
202
|
+
errors << "neither response nor assertion were signed"
|
203
|
+
return errors
|
204
|
+
end
|
205
|
+
|
206
|
+
unless sp.private_keys.empty?
|
207
|
+
begin
|
208
|
+
decypted_anything = assertion.decrypt(&find_decryption_key)
|
209
|
+
rescue XMLSec::DecryptionError
|
210
|
+
errors << "unable to decrypt assertion"
|
211
|
+
return errors
|
212
|
+
end
|
213
|
+
|
214
|
+
if decypted_anything
|
215
|
+
super()
|
216
|
+
return errors unless errors.empty?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# no error
|
221
|
+
errors
|
222
|
+
end
|
223
|
+
|
80
224
|
# @return [Array<Assertion>]
|
81
225
|
def assertions
|
82
226
|
unless instance_variable_defined?(:@assertions)
|
@@ -88,10 +232,14 @@ module SAML2
|
|
88
232
|
# (see Signable#sign)
|
89
233
|
# Signs each assertion.
|
90
234
|
def sign(x509_certificate, private_key, algorithm_name = :sha256)
|
91
|
-
assertions.each { |assertion| assertion.sign(x509_certificate, private_key, algorithm_name) }
|
92
235
|
# make sure we no longer pretty print this object
|
93
236
|
@pretty = false
|
94
|
-
|
237
|
+
|
238
|
+
# if there are no assertions (encrypted?), just sign the response itself
|
239
|
+
return super if assertions.empty?
|
240
|
+
|
241
|
+
assertions.each { |assertion| assertion.sign(x509_certificate, private_key, algorithm_name) }
|
242
|
+
self
|
95
243
|
end
|
96
244
|
|
97
245
|
private
|