saml2 2.1.0 → 2.2.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/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
|