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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0515744b0635a50a555fd60c1f85bce1852d0ce2
4
- data.tar.gz: 8326c0666b2734045c89b8d7454677037b3af01f
3
+ metadata.gz: 0ebef3199d5a8a66f49c3c57a2aafb1a4f54149c
4
+ data.tar.gz: 9dce4bab8931cd0fc1aa8b147a461bdaf198328f
5
5
  SHA512:
6
- metadata.gz: 6e1648fdcab73f9b17a64632d884ab12ceb7ef92e1df7d6dc0e71371fbc74fd693ae6280373efe83ba80916cfe04dc0fb4357bd4b591c62885aa5c29c1cd06a4
7
- data.tar.gz: c07432a96634cf785e6a36bef6e690d098863ebc24c311872c021ffc72fa070ff55b69d3b143527e41fdf26bae3696a286d7dff90ef101f48dff6c89db3ac149
6
+ metadata.gz: a6f4922d39dd247dd91caa3262ed5cb0119c2f2d245c1609a5fa41bee27742a47845fea3dfe6605d7124030f9e7f844c2029e75d2cafbbf57288ded697fa14fa
7
+ data.tar.gz: 99d7c34ce9fae83d81ce16eec10fd6ef9fd1d70f8d67cfbb412d4be45e59b2f28f2db5818de8c7520756a508c9e973c6598571054bb52cda6b5d57fcf8af2c77
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'saml/kit'
3
+
4
+ saml = STDIN.read
5
+
6
+ binding = Saml::Kit::Bindings::HttpPost.new(location: '')
7
+ xml = binding.deserialize('SAMLRequest' => saml).to_xml
8
+ puts Nokogiri::XML(xml).to_xml(indent: 2)
data/lib/saml/kit.rb CHANGED
@@ -3,6 +3,7 @@ require "saml/kit/version"
3
3
  require "active_model"
4
4
  require "active_support/core_ext/date/calculations"
5
5
  require "active_support/core_ext/hash/conversions"
6
+ require "active_support/core_ext/hash/indifferent_access"
6
7
  require "active_support/core_ext/numeric/time"
7
8
  require "active_support/duration"
8
9
  require "builder"
@@ -21,14 +22,14 @@ require "saml/kit/trustable"
21
22
  require "saml/kit/document"
22
23
 
23
24
  require "saml/kit/authentication_request"
24
- require "saml/kit/binding"
25
+ require "saml/kit/bindings"
25
26
  require "saml/kit/configuration"
27
+ require "saml/kit/crypto"
28
+ require "saml/kit/cryptography"
26
29
  require "saml/kit/default_registry"
27
30
  require "saml/kit/fingerprint"
28
31
  require "saml/kit/logout_response"
29
32
  require "saml/kit/logout_request"
30
- require "saml/kit/http_post_binding"
31
- require "saml/kit/http_redirect_binding"
32
33
  require "saml/kit/metadata"
33
34
  require "saml/kit/response"
34
35
  require "saml/kit/identity_provider_metadata"
@@ -36,7 +37,6 @@ require "saml/kit/invalid_document"
36
37
  require "saml/kit/self_signed_certificate"
37
38
  require "saml/kit/service_provider_metadata"
38
39
  require "saml/kit/signature"
39
- require "saml/kit/url_builder"
40
40
  require "saml/kit/xml"
41
41
 
42
42
  I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
@@ -2,18 +2,13 @@ module Saml
2
2
  module Kit
3
3
  class AuthenticationRequest < Document
4
4
  include Requestable
5
- validates_presence_of :acs_url, if: :expected_type?
6
5
 
7
6
  def initialize(xml)
8
7
  super(xml, name: "AuthnRequest")
9
8
  end
10
9
 
11
10
  def acs_url
12
- #if signed? && trusted?
13
- to_h[name]['AssertionConsumerServiceURL'] || registered_acs_url(binding: :post)
14
- #else
15
- #registered_acs_url
16
- #end
11
+ to_h[name]['AssertionConsumerServiceURL']
17
12
  end
18
13
 
19
14
  def name_id_format
@@ -26,11 +21,6 @@ module Saml
26
21
 
27
22
  private
28
23
 
29
- def registered_acs_url(binding:)
30
- return if provider.nil?
31
- provider.assertion_consumer_service_for(binding: binding).try(:location)
32
- end
33
-
34
24
  class Builder
35
25
  attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
36
26
  attr_accessor :version
@@ -45,10 +35,10 @@ module Saml
45
35
  end
46
36
 
47
37
  def to_xml
48
- Signature.sign(id, sign: sign) do |xml, signature|
38
+ Signature.sign(sign: sign) do |xml, signature|
49
39
  xml.tag!('samlp:AuthnRequest', request_options) do
50
40
  xml.tag!('saml:Issuer', issuer)
51
- signature.template(xml)
41
+ signature.template(id)
52
42
  xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
53
43
  end
54
44
  end
@@ -0,0 +1,45 @@
1
+ require "saml/kit/bindings/binding"
2
+ require "saml/kit/bindings/http_post"
3
+ require "saml/kit/bindings/http_redirect"
4
+ require "saml/kit/bindings/url_builder"
5
+
6
+ module Saml
7
+ module Kit
8
+ module Bindings
9
+ HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
10
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
11
+ HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
12
+ ALL = {
13
+ http_post: HTTP_POST,
14
+ http_redirect: HTTP_REDIRECT,
15
+ http_artifact: HTTP_ARTIFACT,
16
+ }
17
+
18
+ def self.binding_for(binding)
19
+ ALL[binding]
20
+ end
21
+
22
+ def self.to_symbol(binding)
23
+ case binding
24
+ when HTTP_REDIRECT
25
+ :http_redirect
26
+ when HTTP_POST
27
+ :http_post
28
+ else
29
+ binding
30
+ end
31
+ end
32
+
33
+ def self.create_for(binding, location)
34
+ case binding
35
+ when HTTP_REDIRECT
36
+ HttpRedirect.new(location: location)
37
+ when HTTP_POST
38
+ HttpPost.new(location: location)
39
+ else
40
+ Binding.new(binding: binding, location: location)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ module Saml
2
+ module Kit
3
+ module Bindings
4
+ class Binding
5
+ attr_reader :binding, :location
6
+
7
+ def initialize(binding:, location:)
8
+ @binding = binding
9
+ @location = location
10
+ end
11
+
12
+ def binding?(other)
13
+ binding == other
14
+ end
15
+
16
+ def serialize(builder, relay_state: nil)
17
+ []
18
+ end
19
+
20
+ def deserialize(params)
21
+ raise ArgumentError.new("Unsupported binding")
22
+ end
23
+
24
+ def to_h
25
+ { binding: binding, location: location }
26
+ end
27
+
28
+ protected
29
+
30
+ def saml_param_from(params)
31
+ if params['SAMLRequest'].present?
32
+ params['SAMLRequest']
33
+ elsif params['SAMLResponse'].present?
34
+ params['SAMLResponse']
35
+ else
36
+ raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ module Saml
2
+ module Kit
3
+ module Bindings
4
+ class HttpPost < Binding
5
+ include Serializable
6
+
7
+ def initialize(location:)
8
+ super(binding: Saml::Kit::Bindings::HTTP_POST, location: location)
9
+ end
10
+
11
+ def serialize(builder, relay_state: nil)
12
+ builder.sign = true
13
+ builder.destination = location
14
+ document = builder.build
15
+ saml_params = {
16
+ document.query_string_parameter => Base64.strict_encode64(document.to_xml),
17
+ }
18
+ saml_params['RelayState'] = relay_state if relay_state.present?
19
+ [location, saml_params]
20
+ end
21
+
22
+ def deserialize(params)
23
+ xml = decode(saml_param_from(params))
24
+ Saml::Kit::Document.to_saml_document(xml)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+ module Saml
2
+ module Kit
3
+ module Bindings
4
+ class HttpRedirect < Binding
5
+ include Serializable
6
+
7
+ def initialize(location:)
8
+ super(binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location)
9
+ end
10
+
11
+ def serialize(builder, relay_state: nil)
12
+ builder.sign = false
13
+ builder.destination = location
14
+ document = builder.build
15
+ [UrlBuilder.new.build(document, relay_state: relay_state), {}]
16
+ end
17
+
18
+ def deserialize(params)
19
+ document = deserialize_document_from!(params)
20
+ ensure_valid_signature!(params, document)
21
+ document.signature_verified!
22
+ document
23
+ end
24
+
25
+ private
26
+
27
+ def deserialize_document_from!(params)
28
+ xml = inflate(decode(unescape(saml_param_from(params))))
29
+ Saml::Kit.logger.debug(xml)
30
+ Saml::Kit::Document.to_saml_document(xml)
31
+ end
32
+
33
+ def ensure_valid_signature!(params, document)
34
+ return if params['Signature'].blank? || params['SigAlg'].blank?
35
+
36
+ signature = decode(params['Signature'])
37
+ canonical_form = ['SAMLRequest', 'SAMLResponse', 'RelayState', 'SigAlg'].map do |key|
38
+ value = params[key]
39
+ value.present? ? "#{key}=#{value}" : nil
40
+ end.compact.join('&')
41
+
42
+ valid = document.provider.verify(algorithm_for(params['SigAlg']), signature, canonical_form)
43
+ raise ArgumentError.new("Invalid Signature") unless valid
44
+ end
45
+
46
+ def algorithm_for(algorithm)
47
+ case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
48
+ when 256
49
+ OpenSSL::Digest::SHA256.new
50
+ when 384
51
+ OpenSSL::Digest::SHA384.new
52
+ when 512
53
+ OpenSSL::Digest::SHA512.new
54
+ else
55
+ OpenSSL::Digest::SHA1.new
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,40 @@
1
+ module Saml
2
+ module Kit
3
+ module Bindings
4
+ class UrlBuilder
5
+ include Serializable
6
+
7
+ def initialize(private_key: Saml::Kit.configuration.signing_private_key)
8
+ @private_key = private_key
9
+ end
10
+
11
+ def build(saml_document, relay_state: nil)
12
+ payload = canonicalize(saml_document, relay_state)
13
+ "#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :private_key
19
+
20
+ def signature_for(payload)
21
+ encode(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
22
+ end
23
+
24
+ def canonicalize(saml_document, relay_state)
25
+ {
26
+ saml_document.query_string_parameter => serialize(saml_document.to_xml),
27
+ 'RelayState' => relay_state,
28
+ 'SigAlg' => Saml::Kit::Namespaces::SHA256,
29
+ }.map do |(key, value)|
30
+ value.present? ? "#{key}=#{escape(value)}" : nil
31
+ end.compact.join('&')
32
+ end
33
+
34
+ def serialize(value)
35
+ encode(deflate(value))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -7,6 +7,7 @@ module Saml
7
7
  attr_accessor :issuer
8
8
  attr_accessor :signature_method, :digest_method
9
9
  attr_accessor :signing_certificate_pem, :signing_private_key_pem, :signing_private_key_password
10
+ attr_accessor :encryption_certificate_pem, :encryption_private_key_pem, :encryption_private_key_password
10
11
  attr_accessor :registry, :session_timeout
11
12
  attr_accessor :logger
12
13
 
@@ -14,23 +15,43 @@ module Saml
14
15
  @signature_method = :SHA256
15
16
  @digest_method = :SHA256
16
17
  @signing_private_key_password = SecureRandom.uuid
18
+ @encryption_private_key_password = SecureRandom.uuid
17
19
  @signing_certificate_pem, @signing_private_key_pem = SelfSignedCertificate.new(@signing_private_key_password).create
20
+ @encryption_certificate_pem, @encryption_private_key_pem = SelfSignedCertificate.new(@encryption_private_key_password).create
18
21
  @registry = DefaultRegistry.new
19
22
  @session_timeout = 3.hours
20
23
  @logger = Logger.new(STDOUT)
21
24
  end
22
25
 
23
26
  def stripped_signing_certificate
24
- signing_certificate_pem.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
27
+ normalize(signing_certificate_pem)
28
+ end
29
+
30
+ def stripped_encryption_certificate
31
+ normalize(encryption_certificate_pem)
25
32
  end
26
33
 
27
34
  def signing_x509
28
35
  OpenSSL::X509::Certificate.new(signing_certificate_pem)
29
36
  end
30
37
 
38
+ def encryption_x509
39
+ OpenSSL::X509::Certificate.new(encryption_certificate_pem)
40
+ end
41
+
31
42
  def signing_private_key
32
43
  OpenSSL::PKey::RSA.new(signing_private_key_pem, signing_private_key_password)
33
44
  end
45
+
46
+ def encryption_private_key
47
+ OpenSSL::PKey::RSA.new(encryption_private_key_pem, encryption_private_key_password)
48
+ end
49
+
50
+ private
51
+
52
+ def normalize(certificate)
53
+ certificate.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
54
+ end
34
55
  end
35
56
  end
36
57
  end
@@ -0,0 +1,16 @@
1
+ require 'saml/kit/crypto/oaep_cipher'
2
+ require 'saml/kit/crypto/rsa_cipher'
3
+ require 'saml/kit/crypto/simple_cipher'
4
+ require 'saml/kit/crypto/unknown_cipher'
5
+
6
+ module Saml
7
+ module Kit
8
+ module Crypto
9
+ DECRYPTORS = [ SimpleCipher, RsaCipher, OaepCipher, UnknownCipher ]
10
+
11
+ def self.decryptor_for(algorithm, key)
12
+ DECRYPTORS.find { |x| x.matches?(algorithm) }.new(algorithm, key)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Saml
2
+ module Kit
3
+ module Crypto
4
+ class OaepCipher
5
+ ALGORITHMS = {
6
+ 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' => true,
7
+ }
8
+ def initialize(algorithm, key)
9
+ @key = key
10
+ end
11
+
12
+ def self.matches?(algorithm)
13
+ ALGORITHMS[algorithm]
14
+ end
15
+
16
+ def decrypt(cipher_text)
17
+ @key.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Saml
2
+ module Kit
3
+ module Crypto
4
+ class RsaCipher
5
+ ALGORITHMS = {
6
+ 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' => true,
7
+ }
8
+
9
+ def initialize(algorithm, key)
10
+ @key = key
11
+ end
12
+
13
+ def self.matches?(algorithm)
14
+ ALGORITHMS[algorithm]
15
+ end
16
+
17
+ def decrypt(cipher_text)
18
+ @key.private_decrypt(cipher_text)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end