saml-kit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/exe/saml-kit-decode-http-post +8 -0
  3. data/lib/saml/kit.rb +4 -4
  4. data/lib/saml/kit/authentication_request.rb +3 -13
  5. data/lib/saml/kit/bindings.rb +45 -0
  6. data/lib/saml/kit/bindings/binding.rb +42 -0
  7. data/lib/saml/kit/bindings/http_post.rb +29 -0
  8. data/lib/saml/kit/bindings/http_redirect.rb +61 -0
  9. data/lib/saml/kit/bindings/url_builder.rb +40 -0
  10. data/lib/saml/kit/configuration.rb +22 -1
  11. data/lib/saml/kit/crypto.rb +16 -0
  12. data/lib/saml/kit/crypto/oaep_cipher.rb +22 -0
  13. data/lib/saml/kit/crypto/rsa_cipher.rb +23 -0
  14. data/lib/saml/kit/crypto/simple_cipher.rb +38 -0
  15. data/lib/saml/kit/crypto/unknown_cipher.rb +18 -0
  16. data/lib/saml/kit/cryptography.rb +30 -0
  17. data/lib/saml/kit/default_registry.rb +1 -0
  18. data/lib/saml/kit/document.rb +6 -2
  19. data/lib/saml/kit/identity_provider_metadata.rb +9 -9
  20. data/lib/saml/kit/locales/en.yml +4 -3
  21. data/lib/saml/kit/logout_request.rb +2 -2
  22. data/lib/saml/kit/logout_response.rb +3 -3
  23. data/lib/saml/kit/metadata.rb +12 -37
  24. data/lib/saml/kit/namespaces.rb +1 -13
  25. data/lib/saml/kit/respondable.rb +4 -0
  26. data/lib/saml/kit/response.rb +120 -37
  27. data/lib/saml/kit/service_provider_metadata.rb +16 -7
  28. data/lib/saml/kit/signature.rb +16 -13
  29. data/lib/saml/kit/trustable.rb +14 -6
  30. data/lib/saml/kit/version.rb +1 -1
  31. data/lib/saml/kit/xml.rb +19 -3
  32. data/saml-kit.gemspec +2 -2
  33. metadata +23 -14
  34. data/lib/saml/kit/binding.rb +0 -40
  35. data/lib/saml/kit/http_post_binding.rb +0 -27
  36. data/lib/saml/kit/http_redirect_binding.rb +0 -58
  37. data/lib/saml/kit/url_builder.rb +0 -38
@@ -14,7 +14,7 @@ module Saml
14
14
  end
15
15
 
16
16
  def want_assertions_signed
17
- attribute = find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
17
+ attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
18
18
  attribute.text.downcase == "true"
19
19
  end
20
20
 
@@ -35,19 +35,19 @@ module Saml
35
35
  @want_assertions_signed = true
36
36
  end
37
37
 
38
- def add_assertion_consumer_service(url, binding: :post)
39
- @acs_urls.push(location: url, binding: Namespaces.binding_for(binding))
38
+ def add_assertion_consumer_service(url, binding: :http_post)
39
+ @acs_urls.push(location: url, binding: Bindings.binding_for(binding))
40
40
  end
41
41
 
42
- def add_single_logout_service(url, binding: :post)
43
- @logout_urls.push(location: url, binding: Namespaces.binding_for(binding))
42
+ def add_single_logout_service(url, binding: :http_post)
43
+ @logout_urls.push(location: url, binding: Bindings.binding_for(binding))
44
44
  end
45
45
 
46
46
  def to_xml
47
- Signature.sign(id, sign: sign) do |xml, signature|
47
+ Signature.sign(sign: sign) do |xml, signature|
48
48
  xml.instruct!
49
49
  xml.EntityDescriptor entity_descriptor_options do
50
- signature.template(xml)
50
+ signature.template(id)
51
51
  xml.SPSSODescriptor descriptor_options do
52
52
  if @configuration.signing_certificate_pem.present?
53
53
  xml.KeyDescriptor use: "signing" do
@@ -58,6 +58,15 @@ module Saml
58
58
  end
59
59
  end
60
60
  end
61
+ if @configuration.encryption_certificate_pem.present?
62
+ xml.KeyDescriptor use: "encryption" do
63
+ xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
64
+ xml.X509Data do
65
+ xml.X509Certificate @configuration.stripped_encryption_certificate
66
+ end
67
+ end
68
+ end
69
+ end
61
70
  logout_urls.each do |item|
62
71
  xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
63
72
  end
@@ -16,17 +16,19 @@ module Saml
16
16
  SHA512: "http://www.w3.org/2001/04/xmlenc#sha512",
17
17
  }.freeze
18
18
 
19
- attr_reader :configuration, :reference_id, :sign
19
+ attr_reader :configuration, :sign, :xml
20
20
 
21
- def initialize(reference_id, configuration: Saml::Kit.configuration, sign: true)
22
- @reference_id = reference_id
21
+ def initialize(xml, configuration:, sign: true)
22
+ @xml = xml
23
23
  @configuration = configuration
24
24
  @sign = sign
25
+ @reference_ids = []
25
26
  end
26
27
 
27
- def template(xml = ::Builder::XmlMarkup.new)
28
+ def template(reference_id)
28
29
  return unless sign
29
30
  return if reference_id.blank?
31
+ @reference_ids << reference_id
30
32
 
31
33
  xml.Signature "xmlns" => Namespaces::XMLDSIG do
32
34
  xml.SignedInfo do
@@ -50,19 +52,20 @@ module Saml
50
52
  end
51
53
  end
52
54
 
53
- def finalize(xml)
54
- if sign && reference_id.present?
55
- document = Xmldsig::SignedDocument.new(xml.target!)
56
- document.sign(private_key)
57
- else
58
- xml.target!
55
+ def finalize
56
+ return xml.target! unless sign
57
+
58
+ raw_xml = xml.target!
59
+ @reference_ids.each do |reference_id|
60
+ raw_xml = Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
59
61
  end
62
+ raw_xml
60
63
  end
61
64
 
62
- def self.sign(id, sign: true, xml: ::Builder::XmlMarkup.new)
63
- signature = new(id, sign: sign)
65
+ def self.sign(sign: true, xml: ::Builder::XmlMarkup.new, configuration: Saml::Kit.configuration)
66
+ signature = new(xml, sign: sign, configuration: configuration)
64
67
  yield xml, signature
65
- signature.finalize(xml)
68
+ signature.finalize
66
69
  end
67
70
 
68
71
  private
@@ -4,8 +4,9 @@ module Saml
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- validate :must_have_valid_signature
7
+ validate :must_have_valid_signature, unless: :signature_manually_verified
8
8
  validate :must_be_registered
9
+ validate :must_be_trusted, unless: :signature_manually_verified
9
10
  end
10
11
 
11
12
  def certificate
@@ -19,7 +20,7 @@ module Saml
19
20
  end
20
21
 
21
22
  def signed?
22
- to_h[name]['Signature'].present?
23
+ to_h.fetch(name, {}).fetch('Signature', nil).present?
23
24
  end
24
25
 
25
26
  def trusted?
@@ -36,8 +37,14 @@ module Saml
36
37
  Saml::Kit.configuration.registry
37
38
  end
38
39
 
40
+ def signature_verified!
41
+ @signature_manually_verified = true
42
+ end
43
+
39
44
  private
40
45
 
46
+ attr_reader :signature_manually_verified
47
+
41
48
  def must_have_valid_signature
42
49
  return if to_xml.blank?
43
50
 
@@ -50,10 +57,11 @@ module Saml
50
57
 
51
58
  def must_be_registered
52
59
  return unless expected_type?
53
- if provider.nil?
54
- errors[:provider] << error_message(:unregistered)
55
- return
56
- end
60
+ return if provider.present?
61
+ errors[:provider] << error_message(:unregistered)
62
+ end
63
+
64
+ def must_be_trusted
57
65
  return if trusted?
58
66
  errors[:fingerprint] << error_message(:invalid_fingerprint)
59
67
  end
@@ -1,5 +1,5 @@
1
1
  module Saml
2
2
  module Kit
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/saml/kit/xml.rb CHANGED
@@ -2,6 +2,12 @@ module Saml
2
2
  module Kit
3
3
  class Xml
4
4
  include ActiveModel::Validations
5
+ NAMESPACES = {
6
+ "NameFormat": Namespaces::ATTR_SPLAT,
7
+ "ds": Namespaces::XMLDSIG,
8
+ "md": Namespaces::METADATA,
9
+ "saml": Namespaces::ASSERTION,
10
+ }.freeze
5
11
 
6
12
  attr_reader :raw_xml, :document
7
13
 
@@ -10,9 +16,7 @@ module Saml
10
16
 
11
17
  def initialize(raw_xml)
12
18
  @raw_xml = raw_xml
13
- @document = Nokogiri::XML(raw_xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT) do |config|
14
- config.noblanks
15
- end
19
+ @document = Nokogiri::XML(raw_xml)
16
20
  end
17
21
 
18
22
  def x509_certificates
@@ -22,6 +26,18 @@ module Saml
22
26
  end
23
27
  end
24
28
 
29
+ def find_by(xpath)
30
+ document.at_xpath(xpath, NAMESPACES)
31
+ end
32
+
33
+ def find_all(xpath)
34
+ document.search(xpath, NAMESPACES)
35
+ end
36
+
37
+ def to_xml(pretty: true)
38
+ pretty ? document.to_xml(indent: 2) : raw_xml
39
+ end
40
+
25
41
  private
26
42
 
27
43
  def validate_signatures
data/saml-kit.gemspec CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency "activemodel", "~> 5.1"
26
- spec.add_dependency "activesupport", "~> 5.1"
25
+ spec.add_dependency "activemodel", ">= 4.2.0"
26
+ spec.add_dependency "activesupport", ">= 4.2.0"
27
27
  spec.add_dependency "builder", "~> 3.2"
28
28
  spec.add_dependency "nokogiri", "~> 1.8"
29
29
  spec.add_dependency "xmldsig", "~> 0.6"
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saml-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-19 00:00:00.000000000 Z
11
+ date: 2017-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.1'
19
+ version: 4.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.1'
26
+ version: 4.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '5.1'
33
+ version: 4.2.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '5.1'
40
+ version: 4.2.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: builder
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -154,6 +154,7 @@ description: A simple toolkit for working with SAML.
154
154
  email:
155
155
  - mo@mokhan.ca
156
156
  executables:
157
+ - saml-kit-decode-http-post
157
158
  - saml-kit-decode-http-redirect
158
159
  extensions: []
159
160
  extra_rdoc_files: []
@@ -167,16 +168,25 @@ files:
167
168
  - Rakefile
168
169
  - bin/console
169
170
  - bin/setup
171
+ - exe/saml-kit-decode-http-post
170
172
  - exe/saml-kit-decode-http-redirect
171
173
  - lib/saml/kit.rb
172
174
  - lib/saml/kit/authentication_request.rb
173
- - lib/saml/kit/binding.rb
175
+ - lib/saml/kit/bindings.rb
176
+ - lib/saml/kit/bindings/binding.rb
177
+ - lib/saml/kit/bindings/http_post.rb
178
+ - lib/saml/kit/bindings/http_redirect.rb
179
+ - lib/saml/kit/bindings/url_builder.rb
174
180
  - lib/saml/kit/configuration.rb
181
+ - lib/saml/kit/crypto.rb
182
+ - lib/saml/kit/crypto/oaep_cipher.rb
183
+ - lib/saml/kit/crypto/rsa_cipher.rb
184
+ - lib/saml/kit/crypto/simple_cipher.rb
185
+ - lib/saml/kit/crypto/unknown_cipher.rb
186
+ - lib/saml/kit/cryptography.rb
175
187
  - lib/saml/kit/default_registry.rb
176
188
  - lib/saml/kit/document.rb
177
189
  - lib/saml/kit/fingerprint.rb
178
- - lib/saml/kit/http_post_binding.rb
179
- - lib/saml/kit/http_redirect_binding.rb
180
190
  - lib/saml/kit/identity_provider_metadata.rb
181
191
  - lib/saml/kit/invalid_document.rb
182
192
  - lib/saml/kit/locales/en.yml
@@ -192,7 +202,6 @@ files:
192
202
  - lib/saml/kit/service_provider_metadata.rb
193
203
  - lib/saml/kit/signature.rb
194
204
  - lib/saml/kit/trustable.rb
195
- - lib/saml/kit/url_builder.rb
196
205
  - lib/saml/kit/version.rb
197
206
  - lib/saml/kit/xml.rb
198
207
  - lib/saml/kit/xsd/MetadataExchange.xsd
@@ -1,40 +0,0 @@
1
- module Saml
2
- module Kit
3
- class Binding
4
- attr_reader :binding, :location
5
-
6
- def initialize(binding:, location:)
7
- @binding = binding
8
- @location = location
9
- end
10
-
11
- def binding?(other)
12
- binding == other
13
- end
14
-
15
- def serialize(builder, relay_state: nil)
16
- []
17
- end
18
-
19
- def deserialize(params)
20
- raise ArgumentError.new("Unsupported binding")
21
- end
22
-
23
- def to_h
24
- { binding: binding, location: location }
25
- end
26
-
27
- protected
28
-
29
- def saml_param_from(params)
30
- if params['SAMLRequest'].present?
31
- params['SAMLRequest']
32
- elsif params['SAMLResponse'].present?
33
- params['SAMLResponse']
34
- else
35
- raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,27 +0,0 @@
1
- module Saml
2
- module Kit
3
- class HttpPostBinding < Binding
4
- include Serializable
5
-
6
- def initialize(location:)
7
- super(binding: Saml::Kit::Namespaces::HTTP_POST, location: location)
8
- end
9
-
10
- def serialize(builder, relay_state: nil)
11
- builder.sign = true
12
- builder.destination = location
13
- document = builder.build
14
- saml_params = {
15
- document.query_string_parameter => Base64.strict_encode64(document.to_xml),
16
- }
17
- saml_params['RelayState'] = relay_state if relay_state.present?
18
- [location, saml_params]
19
- end
20
-
21
- def deserialize(params)
22
- xml = decode(saml_param_from(params))
23
- Saml::Kit::Document.to_saml_document(xml)
24
- end
25
- end
26
- end
27
- end
@@ -1,58 +0,0 @@
1
- module Saml
2
- module Kit
3
- class HttpRedirectBinding < Binding
4
- include Serializable
5
-
6
- def initialize(location:)
7
- super(binding: Saml::Kit::Namespaces::HTTP_REDIRECT, location: location)
8
- end
9
-
10
- def serialize(builder, relay_state: nil)
11
- builder.sign = false
12
- builder.destination = location
13
- document = builder.build
14
- [UrlBuilder.new.build(document, relay_state: relay_state), {}]
15
- end
16
-
17
- def deserialize(params)
18
- document = deserialize_document_from!(params)
19
- ensure_valid_signature!(params, document)
20
- document
21
- end
22
-
23
- private
24
-
25
- def deserialize_document_from!(params)
26
- xml = inflate(decode(unescape(saml_param_from(params))))
27
- Saml::Kit.logger.debug(xml)
28
- Saml::Kit::Document.to_saml_document(xml)
29
- end
30
-
31
- def ensure_valid_signature!(params, document)
32
- return if params['Signature'].blank? || params['SigAlg'].blank?
33
-
34
- signature = decode(params['Signature'])
35
- canonical_form = ['SAMLRequest', 'SAMLResponse', 'RelayState', 'SigAlg'].map do |key|
36
- value = params[key]
37
- value.present? ? "#{key}=#{value}" : nil
38
- end.compact.join('&')
39
-
40
- valid = document.provider.verify(algorithm_for(params['SigAlg']), signature, canonical_form)
41
- raise ArgumentError.new("Invalid Signature") unless valid
42
- end
43
-
44
- def algorithm_for(algorithm)
45
- case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
46
- when 256
47
- OpenSSL::Digest::SHA256.new
48
- when 384
49
- OpenSSL::Digest::SHA384.new
50
- when 512
51
- OpenSSL::Digest::SHA512.new
52
- else
53
- OpenSSL::Digest::SHA1.new
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,38 +0,0 @@
1
- module Saml
2
- module Kit
3
- class UrlBuilder
4
- include Serializable
5
-
6
- def initialize(private_key: Saml::Kit.configuration.signing_private_key)
7
- @private_key = private_key
8
- end
9
-
10
- def build(saml_document, relay_state: nil)
11
- payload = canonicalize(saml_document, relay_state)
12
- "#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
13
- end
14
-
15
- private
16
-
17
- attr_reader :private_key
18
-
19
- def signature_for(payload)
20
- encode(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
21
- end
22
-
23
- def canonicalize(saml_document, relay_state)
24
- {
25
- saml_document.query_string_parameter => serialize(saml_document.to_xml),
26
- 'RelayState' => relay_state,
27
- 'SigAlg' => Saml::Kit::Namespaces::SHA256,
28
- }.map do |(key, value)|
29
- value.present? ? "#{key}=#{escape(value)}" : nil
30
- end.compact.join('&')
31
- end
32
-
33
- def serialize(value)
34
- encode(deflate(value))
35
- end
36
- end
37
- end
38
- end