main
  1# frozen_string_literal: true
  2
  3module Saml
  4  module Kit
  5    # This class is responsible for
  6    # validating an xml digital signature
  7    # in an xml document.
  8    class Signature
  9      include Validatable
 10      include Translatable
 11
 12      validate :validate_signature
 13      validate :validate_certificate
 14
 15      attr_reader :name
 16
 17      def initialize(node)
 18        @name = 'Signature'
 19        @node = node
 20      end
 21
 22      # Returns the embedded X509 Certificate
 23      def certificate
 24        xpath = './ds:KeyInfo/ds:X509Data/ds:X509Certificate'
 25        value = at_xpath(xpath).try(:text)
 26        return if value.nil?
 27
 28        ::Xml::Kit::Certificate.new(value, use: :signing)
 29      end
 30
 31      # Returns true when the fingerprint of the certificate matches one of
 32      # the certificates registered in the metadata.
 33      def trusted?(metadata)
 34        return false if metadata.nil?
 35
 36        metadata.matches?(certificate.fingerprint, use: :signing).present?
 37      end
 38
 39      def digest_value
 40        at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestValue').try(:text)
 41      end
 42
 43      def expected_digest_value
 44        digests = dsignature.references.map do |xxx|
 45          Base64.encode64(xxx.calculate_digest_value).chomp
 46        end
 47        digests.count > 1 ? digests : digests[0]
 48      end
 49
 50      def digest_method
 51        xpath = './ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm'
 52        at_xpath(xpath).try(:value)
 53      end
 54
 55      def signature_value
 56        at_xpath('./ds:SignatureValue').try(:text)
 57      end
 58
 59      def signature_method
 60        at_xpath('./ds:SignedInfo/ds:SignatureMethod/@Algorithm').try(:value)
 61      end
 62
 63      def canonicalization_method
 64        xpath = './ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm'
 65        at_xpath(xpath).try(:value)
 66      end
 67
 68      def transforms
 69        xpath = xpath_for([
 70          '.',
 71          'ds:SignedInfo',
 72          'ds:Reference',
 73          'ds:Transforms',
 74          'ds:Transform',
 75          '@Algorithm',
 76        ])
 77        node.search(xpath, Saml::Kit::Document::NAMESPACES).try(:map, &:value)
 78      end
 79
 80      # Returns the XML Hash.
 81      def to_h
 82        @to_h ||= present? ? Hash.from_xml(to_xml)['Signature'] : {}
 83      end
 84
 85      def present?
 86        node.present?
 87      end
 88
 89      def to_xml(pretty: nil)
 90        pretty ? node.to_xml(indent: 2) : to_s
 91      end
 92
 93      def to_s
 94        node.to_s
 95      end
 96
 97      private
 98
 99      attr_reader :node
100
101      def validate_signature
102        return errors.add(:base, error_message(:empty)) if certificate.nil?
103        return if dsignature.valid?(certificate.x509)
104
105        dsignature.errors.each do |attribute|
106          errors.add(attribute, error_message(attribute))
107        end
108      rescue StandardError => error
109        errors.add(:base, error.message)
110      end
111
112      def validate_certificate(now = Time.now.utc)
113        return unless certificate.present?
114        return if certificate.active?(now)
115
116        message = error_message(
117          :certificate,
118          not_before: certificate.not_before,
119          not_after: certificate.not_after
120        )
121        errors.add(:certificate, message)
122      end
123
124      def at_xpath(xpath)
125        return nil unless node
126
127        node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
128      end
129
130      def dsignature
131        @dsignature ||= Xmldsig::Signature.new(node, 'ID=$uri or @Id')
132      end
133
134      def xpath_for(segments)
135        segments.join('/')
136      end
137    end
138  end
139end