saml_tools 0.0.1

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.
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