saml-kit 0.1.0 → 0.2.0

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