saml_tools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +24 -0
  3. data/README.rdoc +65 -0
  4. data/Rakefile +28 -0
  5. data/lib/saml_tool.rb +23 -0
  6. data/lib/saml_tool/certificate.rb +27 -0
  7. data/lib/saml_tool/decoder.rb +35 -0
  8. data/lib/saml_tool/encoder.rb +31 -0
  9. data/lib/saml_tool/erb_builder.rb +33 -0
  10. data/lib/saml_tool/reader.rb +40 -0
  11. data/lib/saml_tool/redirect.rb +45 -0
  12. data/lib/saml_tool/response_reader.rb +148 -0
  13. data/lib/saml_tool/rsa_key.rb +13 -0
  14. data/lib/saml_tool/saml.rb +30 -0
  15. data/lib/saml_tool/settings.rb +24 -0
  16. data/lib/saml_tool/validator.rb +40 -0
  17. data/lib/saml_tool/version.rb +8 -0
  18. data/lib/saml_tools.rb +1 -0
  19. data/lib/schema/localised-saml-schema-assertion-2.0.xsd +292 -0
  20. data/lib/schema/localised-saml-schema-protocol-2.0.xsd +309 -0
  21. data/lib/schema/localised-xenc-schema.xsd +151 -0
  22. data/lib/schema/xmldsig-core-schema.xsd +318 -0
  23. data/test/files/TEST_FILES.rdoc +22 -0
  24. data/test/files/cacert.pem +21 -0
  25. data/test/files/open_saml_response.xml +56 -0
  26. data/test/files/request.saml.erb +28 -0
  27. data/test/files/response.xml +94 -0
  28. data/test/files/response_template.xml +63 -0
  29. data/test/files/usercert.p12 +0 -0
  30. data/test/files/userkey.pem +18 -0
  31. data/test/files/valid_saml_request.xml +13 -0
  32. data/test/test_helper.rb +51 -0
  33. data/test/units/saml_tool/certificate_test.rb +30 -0
  34. data/test/units/saml_tool/decoder_test.rb +36 -0
  35. data/test/units/saml_tool/encoder_test.rb +38 -0
  36. data/test/units/saml_tool/erb_builder_test.rb +50 -0
  37. data/test/units/saml_tool/reader_test.rb +104 -0
  38. data/test/units/saml_tool/redirect_test.rb +70 -0
  39. data/test/units/saml_tool/response_reader_test.rb +144 -0
  40. data/test/units/saml_tool/rsa_key_test.rb +21 -0
  41. data/test/units/saml_tool/saml_test.rb +21 -0
  42. data/test/units/saml_tool/settings_test.rb +36 -0
  43. data/test/units/saml_tool/validator_test.rb +16 -0
  44. metadata +168 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ M2Q5NTdiZmZmM2FkZTAwNDE4MTg5OTczNWEyODA5ZDNmOTE5ZmMzOQ==
5
+ data.tar.gz: !binary |-
6
+ ZjE1NGRlYzdhMmQzZDg4N2ZiMTZlY2RkZGFjZWViMDNlOWJkOWI5Ng==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZjkwNjQyOTQxNTA4YjAwNmJjZDJjMzBiMTBhZGYxZWYzZDhmNzAzMmI0ZjZh
10
+ NTNlNzY4ODM0MDZmNjg0MGRkOTdhMDIxYjEwYWRhNjUyMTBmZDRlMjNkNGM4
11
+ NzQ2OThjZDcxN2NmNjVkZTM1MTFhNDZjMzk5OGIzN2RlOWI3ODE=
12
+ data.tar.gz: !binary |-
13
+ ZWY0OTc2Y2JiODhlYjIwYjdmY2Y2ZDhkM2Y4MjEzYThiMDAxY2YyNWNmODBl
14
+ ZjVlNWMxZWVlOWM3NTc1YjZkNjM1ZmQyODQxNjE5YzQ4M2UxZTMzOTUxOWIw
15
+ YWUwNjg2ZDk3MDNiNDg5NjFmMmRhYzA4Y2Y4NDVlNWYyNDI2MDI=
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ SAML Tools
2
+
3
+ Copyright (c) 2014 Rob Nichols, Warwickshire County Council
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,65 @@
1
+ =SAML Tools
2
+
3
+ Tools to simplify the creation, validation and sending of SAML objects
4
+
5
+ == SamlTool::Certificate
6
+ Version of OpenSSL::X509::Certificate that adds methods to simplify the retrieval
7
+ of data used in SAML responses.
8
+
9
+ == SamlTool::Decoder
10
+ Decodes base64 and unzips content.
11
+
12
+ == SamlTool::Encoder
13
+ Zips content and base64 encodes it.
14
+
15
+ == SamlTool::ErbBuilder
16
+ Used to build SAML content from erb templates.
17
+
18
+ output = SamlTool::ErbBuilder.build(
19
+ template: '<foo><%= settings %></foo>',
20
+ settings: 'bar'
21
+ )
22
+ output == '<foo>bar</foo>'
23
+
24
+ == SamlTool::Reader
25
+ Wraps SAML documents and exposes data via methods
26
+
27
+ reader = SamlTool::Reader.new(
28
+ output,
29
+ {foo: '//foo/text()'}
30
+ )
31
+ reader.foo == 'bar'
32
+
33
+ == SamlTool::Redirect
34
+ Used to construct redirection uris
35
+
36
+ redirect = Redirect.uri(
37
+ to: 'http://example.com',
38
+ data: {
39
+ foo: 'bar'
40
+ }
41
+ )
42
+ redirect == "http://example.com?foo=bar"
43
+
44
+ == SamlTool::ResponseReader
45
+ A version of SamlTool::Reader tailored for handling SAML responses. It includes
46
+ a valid? method that validates the SAML structure and checks the signature is
47
+ correct.
48
+
49
+ == SamlTool::RsaKey
50
+ Version of OpenSSL::PKey::RSA that adds methods to simplify the retrieval
51
+ of data used in SAML responses.
52
+
53
+ == SamlTool::SAML
54
+ A wrapper for Nokogiri::XML, that applies defaults that are appropriate for SAML
55
+
56
+ == SamlTool::Settings
57
+ Packages up settings so that they can be more easily passed to other objects.
58
+
59
+ == SamlTool::Validator
60
+ Compares documents with SAML schemas to test if they have a valid structure.
61
+
62
+ == Further reading
63
+
64
+ I've {blogged here}[http://undervale.co.uk/blog/?p=490] about some of highs and
65
+ lows of building these tools.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rdoc/task'
5
+ require 'rake/testtask'
6
+
7
+ task :default => :test
8
+
9
+ Rake::RDocTask.new do |rdoc|
10
+ files =['README.rdoc', 'LICENSE', 'lib/**/*.rb']
11
+ rdoc.rdoc_files.add(files)
12
+ rdoc.main = "README.rdoc" # page to start on
13
+ rdoc.title = "SAML Tools Documentation"
14
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
15
+ rdoc.options << '--line-numbers'
16
+ end
17
+
18
+ Rake::TestTask.new do |t|
19
+ t.test_files = FileList['test/**/*_test.rb']
20
+ end
21
+
22
+ task :console do
23
+ require 'irb'
24
+ require 'irb/completion'
25
+ require_relative 'lib/saml_tool'
26
+ ARGV.clear
27
+ IRB.start
28
+ end
data/lib/saml_tool.rb ADDED
@@ -0,0 +1,23 @@
1
+ # Namespace definition
2
+
3
+ require 'nokogiri'
4
+ require 'hashie'
5
+ require 'securerandom'
6
+ require 'base64'
7
+ require 'zlib'
8
+ require 'openssl'
9
+ require 'xmldsig'
10
+
11
+ module SamlTool
12
+ require_relative 'saml_tool/validator'
13
+ require_relative 'saml_tool/erb_builder'
14
+ require_relative 'saml_tool/encoder'
15
+ require_relative 'saml_tool/decoder'
16
+ require_relative 'saml_tool/redirect'
17
+ require_relative 'saml_tool/settings'
18
+ require_relative 'saml_tool/reader'
19
+ require_relative 'saml_tool/response_reader'
20
+ require_relative 'saml_tool/saml'
21
+ require_relative 'saml_tool/certificate'
22
+ require_relative 'saml_tool/rsa_key'
23
+ end
@@ -0,0 +1,27 @@
1
+
2
+ module SamlTool
3
+ class Certificate < OpenSSL::X509::Certificate
4
+
5
+ alias_method :serial_number, :serial
6
+
7
+ def without_leading_and_trailing_labels
8
+ to_s.lines.to_a[1..-2].join
9
+ end
10
+ alias_method :x509_certificate, :without_leading_and_trailing_labels
11
+
12
+ def issuer_name
13
+ @issuer_name ||= slash_list_to_comma_list(issuer)
14
+ end
15
+
16
+ def subject_name
17
+ @subject_name ||= slash_list_to_comma_list(subject)
18
+ end
19
+
20
+ def slash_list_to_comma_list(string)
21
+ string = string.to_s
22
+ string = string[1..-1] if string[0] == '/'
23
+ string.split('/').reverse.join(',')
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+
2
+ module SamlTool
3
+ class Decoder
4
+ attr_reader :saml
5
+ attr_accessor :output
6
+
7
+ def self.decode(encoded_saml)
8
+ new(encoded_saml).decode
9
+ end
10
+
11
+ def initialize(encoded_saml)
12
+ @saml = encoded_saml
13
+ @output = @saml.clone
14
+ end
15
+
16
+ def decode
17
+ base64
18
+ zlib
19
+ output
20
+ end
21
+
22
+ def base64
23
+ self.output = Base64.decode64 output
24
+ end
25
+
26
+ def zlib
27
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS) # I have no idea why we're using minus Zlib::MAX_WBITS. Zlib documentation suggests just Zlib::MAX_WBITS should work, but it doesn't
28
+ self.output = zstream.inflate(output)
29
+ zstream.finish
30
+ zstream.close
31
+ return output
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module SamlTool
3
+ class Encoder
4
+ attr_reader :saml
5
+ attr_accessor :output
6
+
7
+ def self.encode(saml)
8
+ new(saml).encode
9
+ end
10
+
11
+ def initialize(saml)
12
+ @saml = saml
13
+ @output = @saml.clone
14
+ end
15
+
16
+ def encode
17
+ zlib
18
+ base64
19
+ output
20
+ end
21
+
22
+ def base64
23
+ self.output = Base64.encode64 output
24
+ end
25
+
26
+ def zlib
27
+ self.output = Zlib::Deflate.deflate(output, Zlib::BEST_COMPRESSION)[2..-5]
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ require 'erb'
2
+ module SamlTool
3
+ class ErbBuilder
4
+
5
+ attr_reader :args, :settings, :template
6
+
7
+ def self.build(args)
8
+ new(args).to_s
9
+ end
10
+
11
+ def initialize(args)
12
+ @args = args
13
+ @settings = args[:settings]
14
+ @template = args[:template]
15
+ end
16
+
17
+ def to_s
18
+ output
19
+ end
20
+
21
+ def output
22
+ @output ||= build_output
23
+ end
24
+
25
+ def build_output
26
+ erb.result settings.send(:binding)
27
+ end
28
+
29
+ def erb
30
+ ERB.new(template)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+
2
+ module SamlTool
3
+ class Reader
4
+ attr_reader :saml, :config, :namespaces
5
+
6
+ def initialize(saml, config = {}, namespaces = nil)
7
+ @saml = SamlTool::SAML(saml)
8
+ @config = Hashie::Mash.new(config)
9
+ @namespaces = namespaces
10
+ build_methods
11
+ end
12
+
13
+ def to_hash
14
+ config.keys.inject({}){|hash, key| hash[key.to_sym] = send(key.to_sym); hash}
15
+ end
16
+
17
+ private
18
+ def build_methods
19
+ @config.each do |key, value|
20
+ self.class.send :attr_reader, key.to_sym
21
+ source = saml.xpath(value, namespaces)
22
+ content = Content.new(source)
23
+ instance_variable_set("@#{key}".to_sym, content)
24
+ end
25
+ end
26
+
27
+ # A string with memory of the element that was the source of its content.
28
+ # Typically, the source will be a Nokogiri::XML::NodeSet. So:
29
+ # content --> text from an element.
30
+ # content.source --> the Nokogiri NodeSet the text was extracted from.
31
+ class Content < String
32
+ attr_reader :source
33
+ def initialize(source)
34
+ @source = source
35
+ super(source.to_s)
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ module SamlTool
4
+
5
+ class Redirect
6
+ attr_reader :to, :data
7
+
8
+ def self.uri(args)
9
+ new(args).to_s
10
+ end
11
+
12
+ def initialize(args)
13
+ @to = args[:to]
14
+ @data = args[:data]
15
+ end
16
+
17
+ def uri
18
+ @uri || build_uri
19
+ end
20
+
21
+ def data_string
22
+ return data if data.kind_of? String
23
+ data.to_a.collect{|pair| pair.collect{|p| CGI.escape(p.to_s)}.join('=')}.join('&')
24
+ end
25
+
26
+ def append_data
27
+ uri.query = [uri.query, data_string].compact.join('&')
28
+ end
29
+
30
+ def to_s
31
+ uri.to_s
32
+ end
33
+
34
+ def build_uri
35
+ uri_from_to
36
+ append_data
37
+ return uri
38
+ end
39
+
40
+ def uri_from_to
41
+ @uri = URI(to)
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,148 @@
1
+ require "openssl"
2
+
3
+ module SamlTool
4
+ class ResponseReader < Reader
5
+
6
+ # On creation, the keys for this hash will be converted into methods that
7
+ # will return the text gathered at the xpath in the matching value.
8
+ def default_config
9
+ {
10
+ base64_cert: '//ds:X509Certificate/text()',
11
+ canonicalization_method: '//ds:CanonicalizationMethod/@Algorithm',
12
+ reference_uri: '//ds:Reference/@URI',
13
+ inclusive_namespaces: '//ec:InclusiveNamespaces/@PrefixList',
14
+ digest_algorithm: '//ds:DigestMethod/@Algorithm',
15
+ digest_value: '//ds:DigestValue/text()',
16
+ signature_value: '//ds:SignatureValue/text()',
17
+ signature_algorithm: '//ds:SignatureMethod/@Algorithm',
18
+ signed_info: '//ds:SignedInfo'
19
+ }
20
+ end
21
+
22
+ def initialize(saml, config = {}, namespaces = {})
23
+ super(
24
+ saml,
25
+ config.merge(default_config),
26
+ namespaces.merge(default_namespaces)
27
+ )
28
+ end
29
+
30
+ def valid?
31
+ structurally_valid? && signature_verified? && digests_match?
32
+ end
33
+
34
+ def structurally_valid?
35
+ Validator.new(saml.to_s).valid?
36
+ end
37
+
38
+ def signature_verified?
39
+ certificate.public_key.verify(
40
+ signature_algorithm_class.new,
41
+ signature,
42
+ canonicalized_signed_info
43
+ )
44
+ end
45
+
46
+ def digests_match?
47
+ digest_hash == decoded_digest_value
48
+ end
49
+
50
+ def signatureless
51
+ @signatureless ||= clone_saml_and_remove_signature
52
+ end
53
+
54
+ def certificate
55
+ @certificate ||= OpenSSL::X509::Certificate.new(raw_cert)
56
+ end
57
+
58
+ def raw_cert
59
+ @raw_cert ||= Base64.decode64(base64_cert)
60
+ end
61
+
62
+ def fingerprint
63
+ @fingerprint ||= Digest::SHA1.hexdigest(certificate.to_der)
64
+ end
65
+
66
+ def signature
67
+ @signature ||= Base64.decode64(signature_value)
68
+ end
69
+
70
+ def canonicalization_algorithm
71
+ case canonicalization_method
72
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
73
+ when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
74
+ else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
75
+ end
76
+ end
77
+
78
+ def hashed_element
79
+ @hashed_element ||= signatureless.at_xpath("//*[@ID='#{reference_uri[1..-1]}']")
80
+ end
81
+
82
+ def canonicalized_hashed_element
83
+ hashed_element.canonicalize(
84
+ canonicalization_algorithm,
85
+ inclusive_namespaces.split(' ')
86
+ )
87
+ end
88
+
89
+ def canonicalized_signed_info
90
+ signed_info_element.canonicalize(
91
+ canonicalization_algorithm,
92
+ inclusive_namespaces.split(' ')
93
+ )
94
+ end
95
+
96
+ def signed_info_element
97
+ signed_info.source.first
98
+ end
99
+
100
+ def digest_algorithm_class
101
+ @digest_algorithm_class ||= determine_algorithm_class(digest_algorithm)
102
+ end
103
+
104
+ def signature_algorithm_class
105
+ @signature_algorithm_class ||= determine_algorithm_class(signature_algorithm)
106
+ end
107
+
108
+ def determine_algorithm_class(method_text)
109
+ case method_text.slice(/sha(\d+)\s*$/, 1)
110
+ when '256' then OpenSSL::Digest::SHA256
111
+ when '384' then OpenSSL::Digest::SHA384
112
+ when '512' then OpenSSL::Digest::SHA512
113
+ else
114
+ OpenSSL::Digest::SHA1
115
+ end
116
+ end
117
+
118
+ def digest_hash
119
+ @digest_hash ||= digest_algorithm_class.digest(canonicalized_hashed_element)
120
+ end
121
+
122
+ def decoded_digest_value
123
+ Base64.decode64(digest_value)
124
+ end
125
+
126
+ def clone_saml_and_remove_signature
127
+ cloned_saml = saml.clone
128
+ cloned_saml.xpath('//ds:Signature', namespaces).remove
129
+ return SamlTool::SAML(cloned_saml.to_s)
130
+ end
131
+
132
+ def default_namespaces
133
+ {
134
+ ds: dsig,
135
+ ec: c14m
136
+ }
137
+ end
138
+
139
+ def c14m
140
+ 'http://www.w3.org/2001/10/xml-exc-c14n#'
141
+ end
142
+
143
+ def dsig
144
+ 'http://www.w3.org/2000/09/xmldsig#'
145
+ end
146
+
147
+ end
148
+ end