ruby-saml 0.8.8 → 0.8.13
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.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +7 -7
- data/Gemfile +11 -1
- data/README.md +5 -2
- data/Rakefile +0 -14
- data/lib/onelogin/ruby-saml/authrequest.rb +86 -20
- data/lib/onelogin/ruby-saml/logoutrequest.rb +95 -20
- data/lib/onelogin/ruby-saml/logoutresponse.rb +5 -28
- data/lib/onelogin/ruby-saml/metadata.rb +5 -5
- data/lib/onelogin/ruby-saml/response.rb +187 -4
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +146 -10
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
- data/lib/onelogin/ruby-saml/utils.rb +169 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +2 -1
- data/lib/xml_security.rb +330 -78
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/certificates/ruby-saml.crt +14 -0
- data/test/certificates/ruby-saml.key +15 -0
- data/test/logoutrequest_test.rb +177 -44
- data/test/logoutresponse_test.rb +25 -29
- data/test/request_test.rb +100 -37
- data/test/response_test.rb +213 -111
- data/test/responses/adfs_response_xmlns.xml +45 -0
- data/test/responses/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +6 -6
- data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
- data/test/settings_test.rb +7 -7
- data/test/slo_logoutresponse_test.rb +226 -0
- data/test/test_helper.rb +117 -12
- data/test/utils_test.rb +10 -10
- data/test/xml_security_test.rb +310 -68
- metadata +88 -45
@@ -1,15 +1,184 @@
|
|
1
|
+
if RUBY_VERSION < '1.9'
|
2
|
+
require 'uuid'
|
3
|
+
else
|
4
|
+
require 'securerandom'
|
5
|
+
end
|
6
|
+
|
7
|
+
require "base64"
|
8
|
+
require "zlib"
|
9
|
+
|
1
10
|
module OneLogin
|
2
11
|
module RubySaml
|
3
12
|
|
4
13
|
# SAML2 Auxiliary class
|
5
14
|
#
|
6
15
|
class Utils
|
16
|
+
@@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
|
17
|
+
|
18
|
+
BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
|
19
|
+
|
7
20
|
# Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
|
8
21
|
# that there all children other than text nodes can be ignored (e.g. comments). If nil is
|
9
22
|
# passed, nil will be returned.
|
10
23
|
def self.element_text(element)
|
11
24
|
element.texts.map(&:value).join if element
|
12
25
|
end
|
26
|
+
|
27
|
+
# Return a properly formatted x509 certificate
|
28
|
+
#
|
29
|
+
# @param cert [String] The original certificate
|
30
|
+
# @return [String] The formatted certificate
|
31
|
+
#
|
32
|
+
def self.format_cert(cert)
|
33
|
+
# don't try to format an encoded certificate or if is empty or nil
|
34
|
+
if cert.respond_to?(:ascii_only?)
|
35
|
+
return cert if cert.nil? || cert.empty? || !cert.ascii_only?
|
36
|
+
else
|
37
|
+
return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
|
38
|
+
end
|
39
|
+
|
40
|
+
if cert.scan(/BEGIN CERTIFICATE/).length > 1
|
41
|
+
formatted_cert = []
|
42
|
+
cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c|
|
43
|
+
formatted_cert << format_cert(c)
|
44
|
+
}
|
45
|
+
formatted_cert.join("\n")
|
46
|
+
else
|
47
|
+
cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
|
48
|
+
cert = cert.gsub(/\r/, "")
|
49
|
+
cert = cert.gsub(/\n/, "")
|
50
|
+
cert = cert.gsub(/\s/, "")
|
51
|
+
cert = cert.scan(/.{1,64}/)
|
52
|
+
cert = cert.join("\n")
|
53
|
+
"-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return a properly formatted private key
|
58
|
+
#
|
59
|
+
# @param key [String] The original private key
|
60
|
+
# @return [String] The formatted private key
|
61
|
+
#
|
62
|
+
def self.format_private_key(key)
|
63
|
+
# don't try to format an encoded private key or if is empty
|
64
|
+
return key if key.nil? || key.empty? || key.match(/\x0d/)
|
65
|
+
|
66
|
+
# is this an rsa key?
|
67
|
+
rsa_key = key.match("RSA PRIVATE KEY")
|
68
|
+
key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
|
69
|
+
key = key.gsub(/\n/, "")
|
70
|
+
key = key.gsub(/\r/, "")
|
71
|
+
key = key.gsub(/\s/, "")
|
72
|
+
key = key.scan(/.{1,64}/)
|
73
|
+
key = key.join("\n")
|
74
|
+
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
|
75
|
+
"-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Build the Query String signature that will be used in the HTTP-Redirect binding
|
79
|
+
# to generate the Signature
|
80
|
+
# @param params [Hash] Parameters to build the Query String
|
81
|
+
# @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
|
82
|
+
# @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
|
83
|
+
# @option params [String] :relay_state The RelayState parameter
|
84
|
+
# @option params [String] :sig_alg The SigAlg parameter
|
85
|
+
# @return [String] The Query String
|
86
|
+
#
|
87
|
+
def self.build_query(params)
|
88
|
+
type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
|
89
|
+
url_string = "#{type}=#{CGI.escape(data)}"
|
90
|
+
url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
|
91
|
+
url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.uuid
|
95
|
+
RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Build the status error message
|
99
|
+
# @param status_code [String] StatusCode value
|
100
|
+
# @param status_message [Strig] StatusMessage value
|
101
|
+
# @return [String] The status error message
|
102
|
+
def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
|
103
|
+
unless raw_status_code.nil?
|
104
|
+
if raw_status_code.include? "|"
|
105
|
+
status_codes = raw_status_code.split(' | ')
|
106
|
+
values = status_codes.collect do |status_code|
|
107
|
+
status_code.split(':').last
|
108
|
+
end
|
109
|
+
printable_code = values.join(" => ")
|
110
|
+
else
|
111
|
+
printable_code = raw_status_code.split(':').last
|
112
|
+
end
|
113
|
+
error_msg << ', was ' + printable_code
|
114
|
+
end
|
115
|
+
|
116
|
+
unless status_message.nil?
|
117
|
+
error_msg << ' -> ' + status_message
|
118
|
+
end
|
119
|
+
|
120
|
+
error_msg
|
121
|
+
end
|
122
|
+
|
123
|
+
# Base64 decode and try also to inflate a SAML Message
|
124
|
+
# @param saml [String] The deflated and encoded SAML Message
|
125
|
+
# @return [String] The plain SAML Message
|
126
|
+
#
|
127
|
+
def self.decode_raw_saml(saml)
|
128
|
+
return saml unless base64_encoded?(saml)
|
129
|
+
|
130
|
+
decoded = decode(saml)
|
131
|
+
begin
|
132
|
+
inflate(decoded)
|
133
|
+
rescue
|
134
|
+
decoded
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Base 64 decode method
|
139
|
+
# @param string [String] The string message
|
140
|
+
# @return [String] The decoded string
|
141
|
+
#
|
142
|
+
def self.decode(string)
|
143
|
+
Base64.decode64(string)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Base 64 encode method
|
147
|
+
# @param string [String] The string
|
148
|
+
# @return [String] The encoded string
|
149
|
+
#
|
150
|
+
def self.encode(string)
|
151
|
+
if Base64.respond_to?('strict_encode64')
|
152
|
+
Base64.strict_encode64(string)
|
153
|
+
else
|
154
|
+
Base64.encode64(string).gsub(/\n/, "")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check if a string is base64 encoded
|
159
|
+
# @param string [String] string to check the encoding of
|
160
|
+
# @return [true, false] whether or not the string is base64 encoded
|
161
|
+
#
|
162
|
+
def self.base64_encoded?(string)
|
163
|
+
!!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Inflate method
|
167
|
+
# @param deflated [String] The string
|
168
|
+
# @return [String] The inflated string
|
169
|
+
#
|
170
|
+
def self.inflate(deflated)
|
171
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Deflate method
|
175
|
+
# @param inflated [String] The string
|
176
|
+
# @return [String] The deflated string
|
177
|
+
#
|
178
|
+
def self.deflate(inflated)
|
179
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
180
|
+
end
|
181
|
+
|
13
182
|
end
|
14
183
|
end
|
15
184
|
end
|
data/lib/ruby-saml.rb
CHANGED
@@ -2,10 +2,11 @@ require 'onelogin/ruby-saml/logging'
|
|
2
2
|
require 'onelogin/ruby-saml/authrequest'
|
3
3
|
require 'onelogin/ruby-saml/logoutrequest'
|
4
4
|
require 'onelogin/ruby-saml/logoutresponse'
|
5
|
+
require 'onelogin/ruby-saml/slo_logoutresponse'
|
5
6
|
require 'onelogin/ruby-saml/response'
|
6
7
|
require 'onelogin/ruby-saml/settings'
|
7
8
|
require 'onelogin/ruby-saml/utils'
|
8
9
|
require 'onelogin/ruby-saml/validation_error'
|
9
10
|
require 'onelogin/ruby-saml/metadata'
|
10
11
|
require 'onelogin/ruby-saml/version'
|
11
|
-
require 'onelogin/ruby-saml/attributes'
|
12
|
+
require 'onelogin/ruby-saml/attributes'
|
data/lib/xml_security.rb
CHANGED
@@ -34,89 +34,323 @@ require "onelogin/ruby-saml/utils"
|
|
34
34
|
|
35
35
|
module XMLSecurity
|
36
36
|
|
37
|
-
class
|
38
|
-
|
39
|
-
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
37
|
+
class BaseDocument < REXML::Document
|
38
|
+
REXML::Document::entity_expansion_limit = 0
|
40
39
|
|
41
|
-
|
40
|
+
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
|
41
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
42
|
+
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
|
43
|
+
Nokogiri::XML::ParseOptions::NONET
|
42
44
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
45
|
+
def canon_algorithm(element)
|
46
|
+
algorithm = element
|
47
|
+
if algorithm.is_a?(REXML::Element)
|
48
|
+
algorithm = element.attribute('Algorithm').value
|
49
|
+
end
|
50
|
+
|
51
|
+
case algorithm
|
52
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
53
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
54
|
+
Nokogiri::XML::XML_C14N_1_0
|
55
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
56
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
57
|
+
Nokogiri::XML::XML_C14N_1_1
|
58
|
+
else
|
59
|
+
Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
60
|
+
end
|
46
61
|
end
|
47
62
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
cert_text = Base64.decode64(base64_cert)
|
54
|
-
cert = OpenSSL::X509::Certificate.new(cert_text)
|
63
|
+
def algorithm(element)
|
64
|
+
algorithm = element
|
65
|
+
if algorithm.is_a?(REXML::Element)
|
66
|
+
algorithm = element.attribute("Algorithm").value
|
67
|
+
end
|
55
68
|
|
56
|
-
|
57
|
-
fingerprint = Digest::SHA1.hexdigest(cert.to_der)
|
69
|
+
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
|
58
70
|
|
59
|
-
|
60
|
-
|
71
|
+
case algorithm
|
72
|
+
when 256 then OpenSSL::Digest::SHA256
|
73
|
+
when 384 then OpenSSL::Digest::SHA384
|
74
|
+
when 512 then OpenSSL::Digest::SHA512
|
75
|
+
else
|
76
|
+
OpenSSL::Digest::SHA1
|
61
77
|
end
|
78
|
+
end
|
62
79
|
|
63
|
-
|
80
|
+
end
|
81
|
+
|
82
|
+
class Document < BaseDocument
|
83
|
+
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
84
|
+
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
85
|
+
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
|
86
|
+
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
|
87
|
+
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
|
88
|
+
SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
|
89
|
+
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
|
90
|
+
SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
|
91
|
+
ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
92
|
+
INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
|
93
|
+
|
94
|
+
attr_writer :uuid
|
95
|
+
|
96
|
+
def uuid
|
97
|
+
@uuid ||= begin
|
98
|
+
document.root.nil? ? nil : document.root.attributes['ID']
|
99
|
+
end
|
64
100
|
end
|
65
101
|
|
66
|
-
|
67
|
-
|
102
|
+
#<Signature>
|
103
|
+
#<SignedInfo>
|
104
|
+
#<CanonicalizationMethod />
|
105
|
+
#<SignatureMethod />
|
106
|
+
#<Reference>
|
107
|
+
#<Transforms>
|
108
|
+
#<DigestMethod>
|
109
|
+
#<DigestValue>
|
110
|
+
#</Reference>
|
111
|
+
#<Reference /> etc.
|
112
|
+
#</SignedInfo>
|
113
|
+
#<SignatureValue />
|
114
|
+
#<KeyInfo />
|
115
|
+
#<Object />
|
116
|
+
#</Signature>
|
117
|
+
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
|
118
|
+
noko = Nokogiri::XML(self.to_s) do |config|
|
119
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
120
|
+
end
|
68
121
|
|
69
|
-
|
70
|
-
|
122
|
+
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
|
123
|
+
signed_info_element = signature_element.add_element("ds:SignedInfo")
|
124
|
+
signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
|
125
|
+
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm" => signature_method})
|
71
126
|
|
72
|
-
|
127
|
+
# Add Reference
|
128
|
+
reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
|
73
129
|
|
74
|
-
#
|
75
|
-
|
130
|
+
# Add Transforms
|
131
|
+
transforms_element = reference_element.add_element("ds:Transforms")
|
132
|
+
transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
|
133
|
+
c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
|
134
|
+
c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
|
76
135
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
136
|
+
digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method})
|
137
|
+
inclusive_namespaces = INC_PREFIX_LIST.split(" ")
|
138
|
+
canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces)
|
139
|
+
reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
|
140
|
+
|
141
|
+
# add SignatureValue
|
142
|
+
noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
|
143
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
81
144
|
end
|
82
145
|
|
146
|
+
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
|
147
|
+
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
|
83
148
|
|
84
|
-
|
85
|
-
|
86
|
-
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
87
|
-
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
88
|
-
canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
89
|
-
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
90
|
-
noko_sig_element.remove
|
149
|
+
signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
|
150
|
+
signature_element.add_element("ds:SignatureValue").text = signature
|
91
151
|
|
92
|
-
#
|
93
|
-
|
94
|
-
|
152
|
+
# add KeyInfo
|
153
|
+
key_info_element = signature_element.add_element("ds:KeyInfo")
|
154
|
+
x509_element = key_info_element.add_element("ds:X509Data")
|
155
|
+
x509_cert_element = x509_element.add_element("ds:X509Certificate")
|
156
|
+
if certificate.is_a?(String)
|
157
|
+
certificate = OpenSSL::X509::Certificate.new(certificate)
|
158
|
+
end
|
159
|
+
x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
|
160
|
+
|
161
|
+
# add the signature
|
162
|
+
issuer_element = self.elements["//saml:Issuer"]
|
163
|
+
if issuer_element
|
164
|
+
self.root.insert_after issuer_element, signature_element
|
165
|
+
else
|
166
|
+
if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
|
167
|
+
self.root.insert_before sp_sso_descriptor, signature_element
|
168
|
+
else
|
169
|
+
self.root.add_element(signature_element)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
protected
|
175
|
+
|
176
|
+
def compute_signature(private_key, signature_algorithm, document)
|
177
|
+
Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
|
178
|
+
end
|
95
179
|
|
96
|
-
|
97
|
-
|
98
|
-
|
180
|
+
def compute_digest(document, digest_algorithm)
|
181
|
+
digest = digest_algorithm.digest(document)
|
182
|
+
Base64.encode64(digest).strip!
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
class SignedDocument < BaseDocument
|
188
|
+
|
189
|
+
attr_writer :signed_element_id
|
190
|
+
|
191
|
+
def signed_element_id
|
192
|
+
@signed_element_id ||= extract_signed_element_id
|
193
|
+
end
|
99
194
|
|
100
|
-
|
195
|
+
def validate_document(idp_cert_fingerprint, soft = true, options = {})
|
196
|
+
# get cert from response
|
197
|
+
cert_element = REXML::XPath.first(
|
198
|
+
self,
|
199
|
+
"//ds:X509Certificate",
|
200
|
+
{ "ds"=>DSIG }
|
201
|
+
)
|
202
|
+
|
203
|
+
if cert_element
|
204
|
+
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
|
205
|
+
cert_text = Base64.decode64(base64_cert)
|
206
|
+
begin
|
207
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
208
|
+
rescue OpenSSL::X509::CertificateError => _e
|
209
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
|
210
|
+
end
|
211
|
+
|
212
|
+
if options[:fingerprint_alg]
|
213
|
+
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
|
214
|
+
else
|
215
|
+
fingerprint_alg = OpenSSL::Digest::SHA1.new
|
216
|
+
end
|
217
|
+
fingerprint = fingerprint_alg.hexdigest(cert.to_der)
|
218
|
+
|
219
|
+
# check cert matches registered idp cert
|
220
|
+
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
221
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
|
222
|
+
end
|
223
|
+
else
|
224
|
+
if options[:cert]
|
225
|
+
base64_cert = Base64.encode64(options[:cert].to_pem)
|
226
|
+
else
|
227
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
228
|
+
end
|
229
|
+
end
|
230
|
+
validate_signature(base64_cert, soft)
|
231
|
+
end
|
101
232
|
|
102
|
-
|
103
|
-
|
233
|
+
def validate_document_with_cert(idp_cert, soft = true)
|
234
|
+
# get cert from response
|
235
|
+
cert_element = REXML::XPath.first(
|
236
|
+
self,
|
237
|
+
"//ds:X509Certificate",
|
238
|
+
{ "ds"=>DSIG }
|
239
|
+
)
|
240
|
+
|
241
|
+
if cert_element
|
242
|
+
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
|
243
|
+
cert_text = Base64.decode64(base64_cert)
|
244
|
+
begin
|
245
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
246
|
+
rescue OpenSSL::X509::CertificateError => _e
|
247
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate Error"))
|
248
|
+
end
|
104
249
|
|
105
|
-
|
106
|
-
|
250
|
+
# check saml response cert matches provided idp cert
|
251
|
+
if idp_cert.to_pem != cert.to_pem
|
252
|
+
return false
|
107
253
|
end
|
254
|
+
else
|
255
|
+
base64_cert = Base64.encode64(idp_cert.to_pem)
|
108
256
|
end
|
257
|
+
validate_signature(base64_cert, true)
|
258
|
+
end
|
259
|
+
|
260
|
+
def validate_signature(base64_cert, soft = true)
|
109
261
|
|
110
|
-
|
111
|
-
|
262
|
+
document = Nokogiri::XML(self.to_s) do |config|
|
263
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
264
|
+
end
|
112
265
|
|
113
|
-
#
|
114
|
-
|
115
|
-
|
266
|
+
# create a rexml document
|
267
|
+
@working_copy ||= REXML::Document.new(self.to_s).root
|
268
|
+
|
269
|
+
# get signature node
|
270
|
+
sig_element = REXML::XPath.first(
|
271
|
+
@working_copy,
|
272
|
+
"//ds:Signature",
|
273
|
+
{"ds"=>DSIG}
|
274
|
+
)
|
275
|
+
|
276
|
+
if sig_element.nil?
|
277
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("No Signature Node"))
|
278
|
+
end
|
116
279
|
|
117
280
|
# signature method
|
118
|
-
|
281
|
+
sig_alg_value = REXML::XPath.first(
|
282
|
+
sig_element,
|
283
|
+
"./ds:SignedInfo/ds:SignatureMethod",
|
284
|
+
{"ds"=>DSIG}
|
285
|
+
)
|
286
|
+
signature_algorithm = algorithm(sig_alg_value)
|
287
|
+
|
288
|
+
# get signature
|
289
|
+
base64_signature = REXML::XPath.first(
|
290
|
+
sig_element,
|
291
|
+
"./ds:SignatureValue",
|
292
|
+
{"ds" => DSIG}
|
293
|
+
)
|
294
|
+
|
295
|
+
if base64_signature.nil?
|
296
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("SignatureValue not found"))
|
297
|
+
end
|
298
|
+
|
299
|
+
signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
|
300
|
+
|
301
|
+
# canonicalization method
|
302
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
303
|
+
sig_element,
|
304
|
+
'./ds:SignedInfo/ds:CanonicalizationMethod',
|
305
|
+
'ds' => DSIG
|
306
|
+
)
|
307
|
+
|
308
|
+
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
309
|
+
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
310
|
+
|
311
|
+
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
312
|
+
noko_sig_element.remove
|
119
313
|
|
314
|
+
# get inclusive namespaces
|
315
|
+
inclusive_namespaces = extract_inclusive_namespaces
|
316
|
+
|
317
|
+
# check digests
|
318
|
+
ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
|
319
|
+
|
320
|
+
hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
|
321
|
+
|
322
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(
|
323
|
+
ref,
|
324
|
+
'//ds:CanonicalizationMethod',
|
325
|
+
{ "ds" => DSIG }
|
326
|
+
)
|
327
|
+
|
328
|
+
canon_algorithm = process_transforms(ref, canon_algorithm)
|
329
|
+
|
330
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
331
|
+
|
332
|
+
digest_algorithm = algorithm(REXML::XPath.first(
|
333
|
+
ref,
|
334
|
+
"//ds:DigestMethod",
|
335
|
+
{ "ds" => DSIG }
|
336
|
+
))
|
337
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
338
|
+
encoded_digest_value = REXML::XPath.first(
|
339
|
+
ref,
|
340
|
+
"//ds:DigestValue",
|
341
|
+
{ "ds" => DSIG }
|
342
|
+
)
|
343
|
+
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
|
344
|
+
|
345
|
+
unless digests_match?(hash, digest_value)
|
346
|
+
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
|
347
|
+
end
|
348
|
+
|
349
|
+
# get certificate object
|
350
|
+
cert_text = Base64.decode64(base64_cert)
|
351
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
352
|
+
|
353
|
+
# verify signature
|
120
354
|
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
121
355
|
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
|
122
356
|
end
|
@@ -126,43 +360,61 @@ module XMLSecurity
|
|
126
360
|
|
127
361
|
private
|
128
362
|
|
363
|
+
def process_transforms(ref, canon_algorithm)
|
364
|
+
transforms = REXML::XPath.match(
|
365
|
+
ref,
|
366
|
+
"//ds:Transforms/ds:Transform",
|
367
|
+
{ "ds" => DSIG }
|
368
|
+
)
|
369
|
+
|
370
|
+
transforms.each do |transform_element|
|
371
|
+
if transform_element.attributes && transform_element.attributes["Algorithm"]
|
372
|
+
algorithm = transform_element.attributes["Algorithm"]
|
373
|
+
case algorithm
|
374
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
375
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
376
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_0
|
377
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
378
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
379
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_1
|
380
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#",
|
381
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
|
382
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
canon_algorithm
|
388
|
+
end
|
389
|
+
|
129
390
|
def digests_match?(hash, digest_value)
|
130
391
|
hash == digest_value
|
131
392
|
end
|
132
393
|
|
133
394
|
def extract_signed_element_id
|
134
|
-
reference_element
|
135
|
-
|
136
|
-
|
395
|
+
reference_element = REXML::XPath.first(
|
396
|
+
self,
|
397
|
+
"//ds:Signature/ds:SignedInfo/ds:Reference",
|
398
|
+
{"ds"=>DSIG}
|
399
|
+
)
|
137
400
|
|
138
|
-
|
139
|
-
algorithm = element.attribute('Algorithm').value if element
|
140
|
-
case algorithm
|
141
|
-
when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
142
|
-
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
|
143
|
-
when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
|
144
|
-
else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
145
|
-
end
|
146
|
-
end
|
401
|
+
return nil if reference_element.nil?
|
147
402
|
|
148
|
-
|
149
|
-
|
150
|
-
algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
|
151
|
-
case algorithm
|
152
|
-
when 256 then OpenSSL::Digest::SHA256
|
153
|
-
when 384 then OpenSSL::Digest::SHA384
|
154
|
-
when 512 then OpenSSL::Digest::SHA512
|
155
|
-
else
|
156
|
-
OpenSSL::Digest::SHA1
|
157
|
-
end
|
403
|
+
sei = reference_element.attribute("URI").value[1..-1]
|
404
|
+
sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
|
158
405
|
end
|
159
406
|
|
160
407
|
def extract_inclusive_namespaces
|
161
|
-
|
408
|
+
element = REXML::XPath.first(
|
409
|
+
self,
|
410
|
+
"//ec:InclusiveNamespaces",
|
411
|
+
{ "ec" => C14N }
|
412
|
+
)
|
413
|
+
if element
|
162
414
|
prefix_list = element.attributes.get_attribute("PrefixList").value
|
163
415
|
prefix_list.split(" ")
|
164
416
|
else
|
165
|
-
|
417
|
+
nil
|
166
418
|
end
|
167
419
|
end
|
168
420
|
|