saml-kit 1.0.6 → 1.0.7

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +5 -5
  3. data/.rubocop.yml +92 -0
  4. data/.rubocop_todo.yml +45 -0
  5. data/.travis.yml +7 -3
  6. data/Gemfile +2 -2
  7. data/Rakefile +5 -3
  8. data/bin/cibuild +23 -0
  9. data/bin/console +3 -3
  10. data/bin/lint +13 -0
  11. data/bin/setup +1 -1
  12. data/bin/test +19 -0
  13. data/exe/saml-kit-create-self-signed-certificate +6 -6
  14. data/exe/saml-kit-decode-http-redirect +6 -2
  15. data/lib/saml/kit.rb +42 -39
  16. data/lib/saml/kit/assertion.rb +67 -25
  17. data/lib/saml/kit/authentication_request.rb +1 -1
  18. data/lib/saml/kit/bindings.rb +8 -8
  19. data/lib/saml/kit/bindings/binding.rb +5 -5
  20. data/lib/saml/kit/bindings/http_redirect.rb +12 -7
  21. data/lib/saml/kit/bindings/url_builder.rb +2 -2
  22. data/lib/saml/kit/buildable.rb +3 -3
  23. data/lib/saml/kit/builders/assertion.rb +4 -0
  24. data/lib/saml/kit/builders/authentication_request.rb +3 -3
  25. data/lib/saml/kit/builders/logout_request.rb +1 -1
  26. data/lib/saml/kit/builders/logout_response.rb +1 -1
  27. data/lib/saml/kit/builders/response.rb +2 -8
  28. data/lib/saml/kit/builders/templates/assertion.builder +1 -1
  29. data/lib/saml/kit/builders/templates/metadata.builder +4 -4
  30. data/lib/saml/kit/builders/templates/service_provider_metadata.builder +1 -1
  31. data/lib/saml/kit/composite_metadata.rb +9 -5
  32. data/lib/saml/kit/configuration.rb +7 -7
  33. data/lib/saml/kit/default_registry.rb +1 -1
  34. data/lib/saml/kit/document.rb +39 -23
  35. data/lib/saml/kit/identity_provider_metadata.rb +6 -6
  36. data/lib/saml/kit/invalid_document.rb +2 -2
  37. data/lib/saml/kit/locales/en.yml +12 -3
  38. data/lib/saml/kit/logout_request.rb +1 -1
  39. data/lib/saml/kit/logout_response.rb +1 -1
  40. data/lib/saml/kit/metadata.rb +43 -41
  41. data/lib/saml/kit/namespaces.rb +25 -25
  42. data/lib/saml/kit/null_assertion.rb +17 -0
  43. data/lib/saml/kit/respondable.rb +2 -3
  44. data/lib/saml/kit/response.rb +23 -4
  45. data/lib/saml/kit/rspec/have_query_param.rb +1 -1
  46. data/lib/saml/kit/service_provider_metadata.rb +3 -3
  47. data/lib/saml/kit/signature.rb +74 -4
  48. data/lib/saml/kit/translatable.rb +3 -2
  49. data/lib/saml/kit/trustable.rb +4 -11
  50. data/lib/saml/kit/version.rb +1 -1
  51. data/lib/saml/kit/xml_templatable.rb +10 -5
  52. data/saml-kit.gemspec +25 -22
  53. metadata +54 -6
@@ -32,15 +32,15 @@ module Saml
32
32
  # {include:file:spec/examples/identity_provider_metadata_spec.rb}
33
33
  class IdentityProviderMetadata < Metadata
34
34
  def initialize(xml)
35
- super("IDPSSODescriptor", xml)
35
+ super('IDPSSODescriptor', xml)
36
36
  end
37
37
 
38
38
  # Returns the IDPSSODescriptor/@WantAuthnRequestsSigned attribute.
39
39
  def want_authn_requests_signed
40
40
  xpath = "/md:EntityDescriptor/md:#{name}"
41
- attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
41
+ attribute = document.find_by(xpath).attribute('WantAuthnRequestsSigned')
42
42
  return true if attribute.nil?
43
- attribute.text.downcase == "true"
43
+ attribute.text.casecmp('true').zero?
44
44
  end
45
45
 
46
46
  # Returns each of the SingleSignOnService elements.
@@ -59,8 +59,8 @@ module Saml
59
59
  def attributes
60
60
  document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
61
61
  {
62
- format: item.attribute("NameFormat").try(:value),
63
- name: item.attribute("Name").value,
62
+ format: item.attribute('NameFormat').try(:value),
63
+ name: item.attribute('Name').value,
64
64
  }
65
65
  end
66
66
  end
@@ -71,7 +71,7 @@ module Saml
71
71
  # @param relay_state [Object] The RelayState to include the returned SAML params.
72
72
  # @param configuration [Saml::Kit::Configuration] the configuration to use for generating the request.
73
73
  # @return [Array] The url and saml params encoded using the rules for the specified binding.
74
- def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration) # :yields builder
74
+ def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration)
75
75
  builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
76
76
  x.embed_signature = want_authn_requests_signed
77
77
  yield x if block_given?
@@ -7,12 +7,12 @@ module Saml
7
7
  end
8
8
 
9
9
  def initialize(xml, configuration: nil)
10
- super(xml, name: "InvalidDocument")
10
+ super(xml, name: 'InvalidDocument')
11
11
  end
12
12
 
13
13
  def to_h
14
14
  super
15
- rescue
15
+ rescue StandardError
16
16
  {}
17
17
  end
18
18
  end
@@ -3,12 +3,13 @@ en:
3
3
  saml/kit:
4
4
  errors:
5
5
  Assertion:
6
+ cannot_decrypt: "cannot be decrypted."
6
7
  expired: "must not be expired."
7
8
  must_match_issuer: "must match entityId."
8
9
  must_contain_single_assertion: "must contain single Assertion."
9
10
  AuthnRequest:
10
11
  invalid: "must contain AuthnRequest."
11
- invalid_fingerprint: "does not match."
12
+ invalid_fingerprint: "is not registered."
12
13
  unregistered: "is unregistered."
13
14
  IDPSSODescriptor:
14
15
  invalid: "must contain IDPSSODescriptor."
@@ -16,16 +17,24 @@ en:
16
17
  InvalidDocument:
17
18
  invalid: "must contain valid SAMLRequest"
18
19
  LogoutRequest:
19
- invalid_fingerprint: "does not match."
20
+ invalid_fingerprint: "is not registered."
20
21
  unregistered: "is unregistered."
21
22
  LogoutResponse:
22
23
  unregistered: "is unregistered."
24
+ NullAssertion:
25
+ invalid: "is missing."
23
26
  Response:
24
27
  invalid: "must contain Response."
25
- invalid_fingerprint: "does not match."
28
+ invalid_fingerprint: "is not registered."
26
29
  invalid_response_to: "must match request id."
27
30
  invalid_version: "must be 2.0."
28
31
  unregistered: "must originate from registered identity provider."
32
+ must_contain_single_assertion: "must contain a single Assertion."
33
+ Signature:
34
+ certificate: "Not valid before %{not_before}. Not valid after %{not_after}."
35
+ digest_value: "is invalid."
36
+ empty: "is missing."
37
+ signature: "is invalid."
29
38
  SPSSODescriptor:
30
39
  invalid: "must contain SPSSODescriptor."
31
40
  invalid_signature: "invalid signature."
@@ -31,7 +31,7 @@ module Saml
31
31
  # @param xml [String] The raw xml string.
32
32
  # @param configuration [Saml::Kit::Configuration] the configuration to use.
33
33
  def initialize(xml, configuration: Saml::Kit.configuration)
34
- super(xml, name: "LogoutRequest", configuration: configuration)
34
+ super(xml, name: 'LogoutRequest', configuration: configuration)
35
35
  end
36
36
 
37
37
  # Returns the NameID value.
@@ -10,7 +10,7 @@ module Saml
10
10
 
11
11
  def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
12
12
  @request_id = request_id
13
- super(xml, name: "LogoutResponse", configuration: configuration)
13
+ super(xml, name: 'LogoutResponse', configuration: configuration)
14
14
  end
15
15
  end
16
16
  end
@@ -23,7 +23,11 @@ module Saml
23
23
  # for a list of options that can be specified.
24
24
  # {include:file:spec/examples/metadata_spec.rb}
25
25
  class Metadata
26
- METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
26
+ include ActiveModel::Validations
27
+ include XsdValidatable
28
+ include Translatable
29
+ include Buildable
30
+ METADATA_XSD = File.expand_path('./xsd/saml-schema-metadata-2.0.xsd', File.dirname(__FILE__)).freeze
27
31
  NAMESPACES = {
28
32
  "NameFormat": Namespaces::ATTR_SPLAT,
29
33
  "ds": ::Xml::Kit::Namespaces::XMLDSIG,
@@ -31,10 +35,6 @@ module Saml
31
35
  "saml": Namespaces::ASSERTION,
32
36
  "samlp": Namespaces::PROTOCOL,
33
37
  }.freeze
34
- include ActiveModel::Validations
35
- include XsdValidatable
36
- include Translatable
37
- include Buildable
38
38
 
39
39
  validates_presence_of :metadata
40
40
  validate :must_contain_descriptor
@@ -50,7 +50,7 @@ module Saml
50
50
 
51
51
  # Returns the /EntityDescriptor/@entityID
52
52
  def entity_id
53
- document.find_by("/md:EntityDescriptor/@entityID").value
53
+ document.find_by('/md:EntityDescriptor/@entityID').value
54
54
  end
55
55
 
56
56
  # Returns the supported NameIDFormats.
@@ -60,23 +60,23 @@ module Saml
60
60
 
61
61
  # Returns the Organization Name
62
62
  def organization_name
63
- document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationName").try(:text)
63
+ document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationName').try(:text)
64
64
  end
65
65
 
66
66
  # Returns the Organization URL
67
67
  def organization_url
68
- document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationURL").try(:text)
68
+ document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationURL').try(:text)
69
69
  end
70
70
 
71
71
  # Returns the Company
72
72
  def contact_person_company
73
- document.find_by("/md:EntityDescriptor/md:ContactPerson/md:Company").try(:text)
73
+ document.find_by('/md:EntityDescriptor/md:ContactPerson/md:Company').try(:text)
74
74
  end
75
75
 
76
76
  # Returns each of the X509 certificates.
77
77
  def certificates
78
78
  @certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
79
- cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
79
+ cert = item.at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', NAMESPACES).text
80
80
  attribute = item.attribute('use')
81
81
  use = attribute.nil? ? nil : item.attribute('use').value
82
82
  ::Xml::Kit::Certificate.new(cert, use: use)
@@ -98,8 +98,8 @@ module Saml
98
98
  # @param type [String] the type of service. .E.g. `AssertionConsumerServiceURL`
99
99
  def services(type)
100
100
  document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
101
- binding = item.attribute("Binding").value
102
- location = item.attribute("Location").value
101
+ binding = item.attribute('Binding').value
102
+ location = item.attribute('Location').value
103
103
  Saml::Kit::Bindings.create_for(binding, location)
104
104
  end
105
105
  end
@@ -179,25 +179,31 @@ module Saml
179
179
  end
180
180
  end
181
181
 
182
- # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
183
- #
184
- # @param content [String] the raw metadata XML.
185
- # @return [Saml::Kit::Metadata] the metadata document or subclass.
186
- def self.from(content)
187
- hash = Hash.from_xml(content)
188
- entity_descriptor = hash["EntityDescriptor"]
189
- if entity_descriptor.key?("SPSSODescriptor") && entity_descriptor.key?("IDPSSODescriptor")
190
- Saml::Kit::CompositeMetadata.new(content)
191
- elsif entity_descriptor.keys.include?("SPSSODescriptor")
192
- Saml::Kit::ServiceProviderMetadata.new(content)
193
- elsif entity_descriptor.keys.include?("IDPSSODescriptor")
194
- Saml::Kit::IdentityProviderMetadata.new(content)
182
+ def signature
183
+ @signature ||= Signature.new(at_xpath('/md:EntityDescriptor/ds:Signature'))
184
+ end
185
+
186
+ class << self
187
+ # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
188
+ #
189
+ # @param content [String] the raw metadata XML.
190
+ # @return [Saml::Kit::Metadata] the metadata document or subclass.
191
+ def from(content)
192
+ hash = Hash.from_xml(content)
193
+ entity_descriptor = hash['EntityDescriptor']
194
+ if entity_descriptor.key?('SPSSODescriptor') && entity_descriptor.key?('IDPSSODescriptor')
195
+ Saml::Kit::CompositeMetadata.new(content)
196
+ elsif entity_descriptor.keys.include?('SPSSODescriptor')
197
+ Saml::Kit::ServiceProviderMetadata.new(content)
198
+ elsif entity_descriptor.keys.include?('IDPSSODescriptor')
199
+ Saml::Kit::IdentityProviderMetadata.new(content)
200
+ end
195
201
  end
196
- end
197
202
 
198
- # @!visibility private
199
- def self.builder_class
200
- Saml::Kit::Builders::Metadata
203
+ # @!visibility private
204
+ def builder_class
205
+ Saml::Kit::Builders::Metadata
206
+ end
201
207
  end
202
208
 
203
209
  private
@@ -208,6 +214,10 @@ module Saml
208
214
  @document ||= ::Xml::Kit::Document.new(xml, namespaces: NAMESPACES)
209
215
  end
210
216
 
217
+ def at_xpath(xpath)
218
+ document.find_by(xpath)
219
+ end
220
+
211
221
  def metadata
212
222
  document.find_by("/md:EntityDescriptor/md:#{name}").present?
213
223
  end
@@ -221,20 +231,12 @@ module Saml
221
231
  end
222
232
 
223
233
  def must_have_valid_signature
224
- return if to_xml.blank?
225
-
226
- unless valid_signature?
227
- errors[:base] << error_message(:invalid_signature)
228
- end
229
- end
234
+ return unless signature.present?
230
235
 
231
- def valid_signature?
232
- xml = document
233
- result = xml.valid?
234
- xml.errors.each do |error|
235
- errors[:base] << error
236
+ signature.valid?
237
+ signature.errors.each do |attribute, error|
238
+ errors[attribute] << error
236
239
  end
237
- result
238
240
  end
239
241
  end
240
242
  end
@@ -1,32 +1,32 @@
1
1
  module Saml
2
2
  module Kit
3
3
  module Namespaces
4
- SAML_2_0 = "urn:oasis:names:tc:SAML:2.0"
5
- SAML_1_1 = "urn:oasis:names:tc:SAML:1.1"
6
- ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format"
7
- NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format"
8
- NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format"
9
- STATUS = "#{SAML_2_0}:status"
4
+ SAML_2_0 = 'urn:oasis:names:tc:SAML:2.0'.freeze
5
+ SAML_1_1 = 'urn:oasis:names:tc:SAML:1.1'.freeze
6
+ ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format".freeze
7
+ NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format".freeze
8
+ NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format".freeze
9
+ STATUS = "#{SAML_2_0}:status".freeze
10
10
 
11
- ASSERTION = "#{SAML_2_0}:assertion"
12
- ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*"
13
- BASIC = "#{ATTR_NAME_FORMAT}:basic"
14
- BEARER = "#{SAML_2_0}:cm:bearer"
15
- EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress"
16
- INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy"
17
- METADATA = "#{SAML_2_0}:metadata"
18
- PASSWORD = "#{SAML_2_0}:ac:classes:Password"
19
- PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport"
20
- PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent"
21
- PROTOCOL = "#{SAML_2_0}:protocol"
22
- REQUESTER_ERROR = "#{STATUS}:Requester"
23
- RESPONDER_ERROR = "#{STATUS}:Responder"
24
- SUCCESS = "#{STATUS}:Success"
25
- TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient"
26
- UNSPECIFIED = "#{SAML_2_0}:consent:unspecified"
27
- UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified"
28
- URI = "#{ATTR_NAME_FORMAT}:uri"
29
- VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch"
11
+ ASSERTION = "#{SAML_2_0}:assertion".freeze
12
+ ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*".freeze
13
+ BASIC = "#{ATTR_NAME_FORMAT}:basic".freeze
14
+ BEARER = "#{SAML_2_0}:cm:bearer".freeze
15
+ EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress".freeze
16
+ INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy".freeze
17
+ METADATA = "#{SAML_2_0}:metadata".freeze
18
+ PASSWORD = "#{SAML_2_0}:ac:classes:Password".freeze
19
+ PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport".freeze
20
+ PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent".freeze
21
+ PROTOCOL = "#{SAML_2_0}:protocol".freeze
22
+ REQUESTER_ERROR = "#{STATUS}:Requester".freeze
23
+ RESPONDER_ERROR = "#{STATUS}:Responder".freeze
24
+ SUCCESS = "#{STATUS}:Success".freeze
25
+ TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient".freeze
26
+ UNSPECIFIED = "#{SAML_2_0}:consent:unspecified".freeze
27
+ UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified".freeze
28
+ URI = "#{ATTR_NAME_FORMAT}:uri".freeze
29
+ VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch".freeze
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1,17 @@
1
+ module Saml
2
+ module Kit
3
+ class NullAssertion
4
+ include ActiveModel::Validations
5
+ include Translatable
6
+ validate :invalid
7
+
8
+ def invalid
9
+ errors[:assertion].push(error_message(:invalid))
10
+ end
11
+
12
+ def name
13
+ 'NullAssertion'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -33,10 +33,9 @@ module Saml
33
33
 
34
34
  def must_match_request_id
35
35
  return if request_id.nil?
36
+ return if in_response_to == request_id
36
37
 
37
- if in_response_to != request_id
38
- errors[:in_response_to] << error_message(:invalid_response_to)
39
- end
38
+ errors[:in_response_to] << error_message(:invalid_response_to)
40
39
  end
41
40
  end
42
41
  end
@@ -8,14 +8,23 @@ module Saml
8
8
  def_delegators :assertion, :name_id, :[], :attributes
9
9
 
10
10
  validate :must_be_valid_assertion
11
+ validate :must_contain_single_assertion
11
12
 
12
13
  def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
13
14
  @request_id = request_id
14
- super(xml, name: "Response", configuration: configuration)
15
+ super(xml, name: 'Response', configuration: configuration)
15
16
  end
16
17
 
17
- def assertion
18
- @assertion ||= Saml::Kit::Assertion.new(to_h, configuration: @configuration)
18
+ def assertion(private_keys = configuration.private_keys(use: :encryption))
19
+ @assertion ||=
20
+ begin
21
+ node = assertion_nodes.last
22
+ if node.nil?
23
+ Saml::Kit::NullAssertion.new
24
+ else
25
+ Saml::Kit::Assertion.new(node, configuration: @configuration, private_keys: private_keys)
26
+ end
27
+ end
19
28
  end
20
29
 
21
30
  private
@@ -23,9 +32,19 @@ module Saml
23
32
  def must_be_valid_assertion
24
33
  assertion.valid?
25
34
  assertion.errors.each do |attribute, error|
26
- self.errors[attribute] << error
35
+ attribute = :assertion if attribute == :base
36
+ errors[attribute] << error
27
37
  end
28
38
  end
39
+
40
+ def must_contain_single_assertion
41
+ return if assertion_nodes.count <= 1
42
+ errors[:base] << error_message(:must_contain_single_assertion)
43
+ end
44
+
45
+ def assertion_nodes
46
+ search(Saml::Kit::Assertion::XPATH)
47
+ end
29
48
  end
30
49
  end
31
50
  end
@@ -6,7 +6,7 @@ RSpec::Matchers.define :have_query_param do |key|
6
6
  end
7
7
 
8
8
  def query_params_from(url)
9
- Hash[query_for(url).split("&").map { |x| x.split('=', 2) }]
9
+ Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
10
10
  end
11
11
 
12
12
  def uri_for(url)
@@ -3,7 +3,7 @@ module Saml
3
3
  # {include:file:spec/examples/service_provider_metadata_spec.rb}
4
4
  class ServiceProviderMetadata < Metadata
5
5
  def initialize(xml)
6
- super("SPSSODescriptor", xml)
6
+ super('SPSSODescriptor', xml)
7
7
  end
8
8
 
9
9
  # Returns each of the AssertionConsumerService bindings.
@@ -20,9 +20,9 @@ module Saml
20
20
 
21
21
  # Returns true when the metadata demands that Assertions must be signed.
22
22
  def want_assertions_signed
23
- attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
23
+ attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute('WantAssertionsSigned')
24
24
  return true if attribute.nil?
25
- attribute.text.downcase == "true"
25
+ attribute.text.casecmp('true').zero?
26
26
  end
27
27
 
28
28
  # @!visibility private
@@ -2,14 +2,21 @@ module Saml
2
2
  module Kit
3
3
  class Signature
4
4
  include ActiveModel::Validations
5
+ include Translatable
5
6
 
6
- def initialize(xml_hash)
7
- @xml_hash = xml_hash
7
+ validate :validate_signature
8
+ validate :validate_certificate
9
+
10
+ attr_reader :name
11
+
12
+ def initialize(node)
13
+ @name = 'Signature'
14
+ @node = node
8
15
  end
9
16
 
10
17
  # Returns the embedded X509 Certificate
11
18
  def certificate
12
- value = to_h.fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
19
+ value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
13
20
  return if value.nil?
14
21
  ::Xml::Kit::Certificate.new(value, use: :signing)
15
22
  end
@@ -20,9 +27,72 @@ module Saml
20
27
  metadata.matches?(certificate.fingerprint, use: :signing)
21
28
  end
22
29
 
30
+ def digest_value
31
+ at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestValue').try(:text)
32
+ end
33
+
34
+ def digest_method
35
+ at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm').try(:value)
36
+ end
37
+
38
+ def signature_value
39
+ at_xpath('./ds:SignatureValue').try(:text)
40
+ end
41
+
42
+ def signature_method
43
+ at_xpath('./ds:SignedInfo/ds:SignatureMethod/@Algorithm').try(:value)
44
+ end
45
+
46
+ def canonicalization_method
47
+ at_xpath('./ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm').try(:value)
48
+ end
49
+
50
+ def transforms
51
+ node.search('./ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform/@Algorithm', Saml::Kit::Document::NAMESPACES).try(:map, &:value)
52
+ end
53
+
23
54
  # Returns the XML Hash.
24
55
  def to_h
25
- @xml_hash
56
+ @xml_hash ||= present? ? Hash.from_xml(to_xml)['Signature'] : {}
57
+ end
58
+
59
+ def present?
60
+ node
61
+ end
62
+
63
+ def to_xml
64
+ node.to_s
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :node
70
+
71
+ def validate_signature
72
+ return errors[:base].push(error_message(:empty)) if certificate.nil?
73
+
74
+ signature = Xmldsig::Signature.new(@node, 'ID=$uri or @Id')
75
+ return if signature.valid?(certificate.x509)
76
+ signature.errors.each do |attribute|
77
+ errors.add(attribute, error_message(attribute))
78
+ end
79
+ end
80
+
81
+ def validate_certificate(now = Time.now.utc)
82
+ return unless certificate.present?
83
+ return if certificate.active?(now)
84
+
85
+ message = error_message(
86
+ :certificate,
87
+ not_before: certificate.not_before,
88
+ not_after: certificate.not_after
89
+ )
90
+ errors.add(:certificate, message)
91
+ end
92
+
93
+ def at_xpath(xpath)
94
+ return nil unless node
95
+ node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
26
96
  end
27
97
  end
28
98
  end