ruby-saml-mod 0.1.8 → 0.1.9
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.
- data/lib/onelogin/saml.rb +1 -1
- data/lib/onelogin/saml/logout_response.rb +5 -5
- data/lib/onelogin/saml/response.rb +39 -27
- data/lib/xml_sec.rb +72 -64
- data/ruby-saml-mod.gemspec +3 -2
- metadata +21 -6
data/lib/onelogin/saml.rb
CHANGED
@@ -12,11 +12,11 @@ module Onelogin::Saml
|
|
12
12
|
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
13
13
|
@xml = zlib.inflate(@xml)
|
14
14
|
@document = XMLSecurity::SignedDocument.new(@xml)
|
15
|
-
|
16
|
-
@in_response_to =
|
17
|
-
@destination =
|
18
|
-
@status_code =
|
19
|
-
@status_message =
|
15
|
+
|
16
|
+
@in_response_to = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
|
17
|
+
@destination = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['Destination'] rescue nil
|
18
|
+
@status_code = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)['Value'] rescue nil
|
19
|
+
@status_message = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).content rescue nil
|
20
20
|
end
|
21
21
|
|
22
22
|
def logger=(val)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Onelogin::Saml
|
2
2
|
class Response
|
3
3
|
|
4
|
-
attr_reader :settings, :document, :xml, :response
|
5
|
-
attr_reader :name_id, :name_qualifier, :session_index
|
4
|
+
attr_reader :settings, :document, :decrypted_document, :xml, :response
|
5
|
+
attr_reader :name_id, :name_qualifier, :session_index, :saml_attributes
|
6
6
|
attr_reader :status_code, :status_message
|
7
7
|
attr_reader :in_response_to, :destination
|
8
8
|
attr_reader :validation_error
|
@@ -11,16 +11,24 @@ module Onelogin::Saml
|
|
11
11
|
@settings = settings
|
12
12
|
|
13
13
|
@xml = Base64.decode64(@response)
|
14
|
-
@document =
|
15
|
-
@document.
|
14
|
+
@document = LibXML::XML::Document.string(@xml)
|
15
|
+
@document.extend(XMLSecurity::SignedDocument)
|
16
16
|
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
|
21
|
-
@
|
22
|
-
@
|
23
|
-
@
|
17
|
+
@decrypted_document = LibXML::XML::Document.document(@document)
|
18
|
+
@decrypted_document.extend(XMLSecurity::SignedDocument)
|
19
|
+
@decrypted_document.decrypt(@settings)
|
20
|
+
|
21
|
+
@in_response_to = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
|
22
|
+
@destination = @decrypted_document.find_first("/samlp:Response", Onelogin::NAMESPACES)['Destination'] rescue nil
|
23
|
+
@name_id = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES).content rescue nil
|
24
|
+
@saml_attributes = {}
|
25
|
+
@decrypted_document.find("//saml:Attribute", Onelogin::NAMESPACES).each do |attr|
|
26
|
+
@saml_attributes[attr['FriendlyName']] = attr.content.strip rescue nil
|
27
|
+
end
|
28
|
+
@name_qualifier = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Onelogin::NAMESPACES)["NameQualifier"] rescue nil
|
29
|
+
@session_index = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:AuthnStatement", Onelogin::NAMESPACES)["SessionIndex"] rescue nil
|
30
|
+
@status_code = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)["Value"] rescue nil
|
31
|
+
@status_message = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).content rescue nil
|
24
32
|
end
|
25
33
|
|
26
34
|
def logger=(val)
|
@@ -28,23 +36,27 @@ module Onelogin::Saml
|
|
28
36
|
end
|
29
37
|
|
30
38
|
def is_valid?
|
31
|
-
if
|
32
|
-
if !@settings.idp_cert_fingerprint
|
33
|
-
@validation_error = "No fingerprint configured in SAML settings"
|
34
|
-
false
|
35
|
-
elsif @document.validate(@settings.idp_cert_fingerprint, @logger)
|
36
|
-
true
|
37
|
-
else
|
38
|
-
@validation_error = @document.validation_error
|
39
|
-
false
|
40
|
-
end
|
41
|
-
elsif @response.blank?
|
39
|
+
if @response.nil? || @response == ""
|
42
40
|
@validation_error = "No response to validate"
|
43
|
-
false
|
44
|
-
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
|
44
|
+
if @document.find_first("//ds:X509Certificate", Onelogin::NAMESPACES).nil?
|
45
45
|
@validation_error = "No ds:X509Certificate element"
|
46
|
-
false
|
46
|
+
return false
|
47
47
|
end
|
48
|
+
|
49
|
+
if !@settings.idp_cert_fingerprint
|
50
|
+
@validation_error = "No fingerprint configured in SAML settings"
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
if !@document.validate(@settings.idp_cert_fingerprint, @logger)
|
55
|
+
@validation_error = @document.validation_error
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
true
|
48
60
|
end
|
49
61
|
|
50
62
|
def success_status?
|
@@ -60,8 +72,8 @@ module Onelogin::Saml
|
|
60
72
|
end
|
61
73
|
|
62
74
|
def fingerprint_from_idp
|
63
|
-
if base64_cert = @document.
|
64
|
-
cert_text = Base64.decode64(base64_cert.
|
75
|
+
if base64_cert = @document.find_first("//ds:X509Certificate")
|
76
|
+
cert_text = Base64.decode64(base64_cert.content)
|
65
77
|
cert = OpenSSL::X509::Certificate.new(cert_text)
|
66
78
|
Digest::SHA1.hexdigest(cert.to_der)
|
67
79
|
else
|
data/lib/xml_sec.rb
CHANGED
@@ -23,93 +23,99 @@
|
|
23
23
|
# Portions Copyrighted 2007 Todd W Saxton.
|
24
24
|
|
25
25
|
require 'rubygems'
|
26
|
-
require "
|
27
|
-
require "rexml/xpath"
|
26
|
+
require "xml/libxml"
|
28
27
|
require "openssl"
|
29
|
-
require "xmlcanonicalizer"
|
30
28
|
require "digest/sha1"
|
31
29
|
require "tempfile"
|
32
30
|
require "shellwords"
|
33
31
|
|
34
32
|
module XMLSecurity
|
35
|
-
|
36
|
-
class SignedDocument < REXML::Document
|
37
|
-
|
33
|
+
module SignedDocument
|
38
34
|
attr_reader :validation_error
|
39
35
|
|
40
|
-
def validate
|
36
|
+
def validate(idp_cert_fingerprint, logger = nil)
|
41
37
|
# get cert from response
|
42
|
-
base64_cert
|
43
|
-
cert_text
|
44
|
-
cert
|
45
|
-
|
38
|
+
base64_cert = self.find_first("//ds:X509Certificate", Onelogin::NAMESPACES).content
|
39
|
+
cert_text = Base64.decode64(base64_cert)
|
40
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
41
|
+
|
46
42
|
# check cert matches registered idp cert
|
47
|
-
fingerprint
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
fingerprint = Digest::SHA1.hexdigest(cert.to_der)
|
44
|
+
expected_fingerprint = idp_cert_fingerprint.gsub(":", "").downcase
|
45
|
+
if fingerprint != expected_fingerprint
|
46
|
+
@validation_error = "Invalid fingerprint (expected #{expected_fingerprint}, got #{fingerprint})"
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
53
50
|
validate_doc(base64_cert, logger)
|
54
51
|
end
|
55
|
-
|
52
|
+
|
53
|
+
def canonicalize_node(node)
|
54
|
+
tmp_document = LibXML::XML::Document.new
|
55
|
+
tmp_document.root = tmp_document.import(node)
|
56
|
+
tmp_document.canonicalize
|
57
|
+
end
|
58
|
+
|
56
59
|
def validate_doc(base64_cert, logger)
|
57
60
|
# validate references
|
58
61
|
|
59
|
-
|
60
|
-
sig_element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
|
61
|
-
sig_element.remove
|
62
|
+
sig_element = find_first("//ds:Signature", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
|
62
63
|
|
63
|
-
#check digests
|
64
|
-
|
65
|
-
|
66
|
-
uri
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
if
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
64
|
+
# check digests
|
65
|
+
sig_element.find("//ds:Reference", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).each do |ref|
|
66
|
+
# Find the referenced element
|
67
|
+
uri = ref["URI"]
|
68
|
+
ref_element = find_first("//*[@ID='#{uri[1,uri.size]}']")
|
69
|
+
|
70
|
+
# Create a copy document with it
|
71
|
+
ref_document = LibXML::XML::Document.new
|
72
|
+
ref_document.root = ref_document.import(ref_element)
|
73
|
+
|
74
|
+
# Remove the Signature node
|
75
|
+
ref_document_sig_element = ref_document.find_first("//ds:Signature", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
|
76
|
+
ref_document_sig_element.remove! if ref_document_sig_element
|
77
|
+
|
78
|
+
# Canonicalize the referenced element's document
|
79
|
+
ref_document_canonicalized = ref_document.canonicalize
|
80
|
+
hash = Base64::encode64(Digest::SHA1.digest(ref_document_canonicalized)).chomp
|
81
|
+
digest_value = sig_element.find_first("//ds:DigestValue", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).content
|
82
|
+
|
83
|
+
if hash != digest_value
|
84
|
+
@validation_error = <<-EOF.gsub(/^\s+/, '')
|
85
|
+
Invalid references digest.
|
86
|
+
Got digest of
|
87
|
+
#{hash}
|
88
|
+
but expected
|
89
|
+
#{digest_value}
|
90
|
+
XML from response:
|
91
|
+
#{ref_document.to_s(:indent => false)}
|
92
|
+
Canonized XML:
|
93
|
+
#{ref_document_canonicalized}
|
94
|
+
EOF
|
95
|
+
return false
|
87
96
|
end
|
88
|
-
|
89
|
-
return valid_flag if !valid_flag
|
90
97
|
end
|
91
98
|
|
92
99
|
# verify signature
|
93
|
-
|
94
|
-
|
95
|
-
canon_string = canoner.canonicalize(signed_info_element)
|
100
|
+
signed_info_element = sig_element.find_first("//ds:SignedInfo", { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
|
101
|
+
canon_string = canonicalize_node(signed_info_element)
|
96
102
|
|
97
|
-
base64_signature
|
98
|
-
signature
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
return
|
103
|
+
base64_signature = sig_element.find_first("//ds:SignatureValue", { "ds" => "http://www.w3.org/2000/09/xmldsig#" }).content
|
104
|
+
signature = Base64.decode64(base64_signature)
|
105
|
+
|
106
|
+
cert_text = Base64.decode64(base64_cert)
|
107
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
108
|
+
|
109
|
+
if !cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
|
110
|
+
@validation_error = "Invalid public key"
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
return true
|
108
114
|
end
|
109
115
|
|
110
116
|
def decrypt(settings)
|
111
117
|
if settings.encryption_configured?
|
112
|
-
|
118
|
+
find("//xenc:EncryptedData", Onelogin::NAMESPACES).each do |node|
|
113
119
|
Tempfile.open("ruby-saml-decrypt") do |f|
|
114
120
|
f.puts node.to_s
|
115
121
|
f.close
|
@@ -119,9 +125,11 @@ INFO
|
|
119
125
|
@logger.warn "Could not decrypt: #{decrypted_xml}" if @logger
|
120
126
|
return false
|
121
127
|
else
|
122
|
-
decrypted_doc =
|
128
|
+
decrypted_doc = LibXML::XML::Document.string(decrypted_xml)
|
123
129
|
decrypted_node = decrypted_doc.root
|
124
|
-
|
130
|
+
decrypted_node = self.import(decrypted_node)
|
131
|
+
node.parent.next = decrypted_node
|
132
|
+
node.parent.remove!
|
125
133
|
end
|
126
134
|
f.unlink
|
127
135
|
end
|
data/ruby-saml-mod.gemspec
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = %q{ruby-saml-mod}
|
3
|
-
s.version = "0.1.
|
3
|
+
s.version = "0.1.9"
|
4
4
|
|
5
5
|
s.authors = ["OneLogin LLC", "Bracken", "Zach", "Cody"]
|
6
|
-
s.date = %q{2012-
|
6
|
+
s.date = %q{2012-04-27}
|
7
7
|
s.extra_rdoc_files = [
|
8
8
|
"LICENSE"
|
9
9
|
]
|
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
|
23
23
|
"lib/xml_sec.rb",
|
24
24
|
"ruby-saml-mod.gemspec"
|
25
25
|
]
|
26
|
+
s.add_dependency('libxml-ruby', '>= 2.3.0')
|
26
27
|
s.homepage = %q{http://github.com/bracken/ruby-saml}
|
27
28
|
s.require_paths = ["lib"]
|
28
29
|
s.summary = %q{Ruby library for SAML service providers}
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-saml-mod
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 9
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 9
|
10
|
+
version: 0.1.9
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- OneLogin LLC
|
@@ -18,9 +18,24 @@ autorequire:
|
|
18
18
|
bindir: bin
|
19
19
|
cert_chain: []
|
20
20
|
|
21
|
-
date: 2012-
|
22
|
-
dependencies:
|
23
|
-
|
21
|
+
date: 2012-04-27 00:00:00 Z
|
22
|
+
dependencies:
|
23
|
+
- !ruby/object:Gem::Dependency
|
24
|
+
name: libxml-ruby
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
hash: 3
|
32
|
+
segments:
|
33
|
+
- 2
|
34
|
+
- 3
|
35
|
+
- 0
|
36
|
+
version: 2.3.0
|
37
|
+
type: :runtime
|
38
|
+
version_requirements: *id001
|
24
39
|
description: "This is an early fork from https://github.com/onelogin/ruby-saml - I plan to \"rebase\" these changes ontop of their current version eventually. "
|
25
40
|
email:
|
26
41
|
executables: []
|