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      validate :validate_reference
 15
 16      attr_reader :name
 17
 18      def initialize(node)
 19        @name = 'Signature'
 20        @node = node
 21      end
 22
 23      # Returns the embedded X509 Certificate
 24      #
 25      # Memoized with a `defined?` guard rather than `||=` because a signature
 26      # without KeyInfo legitimately has no certificate, and callers depend on
 27      # the nil.
 28      def certificate
 29        return @certificate if defined?(@certificate)
 30
 31        value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
 32        @certificate =
 33          value.nil? ? nil : ::Xml::Kit::Certificate.new(value, use: :signing)
 34      end
 35
 36      # Returns true when the fingerprint of the certificate matches one of
 37      # the certificates registered in the metadata.
 38      def trusted?(metadata)
 39        return false if metadata.nil?
 40        return false if certificate.nil?
 41        return false unless reference.bound?
 42
 43        metadata.matches?(certificate.fingerprint, use: :signing).present?
 44      end
 45
 46      def digest_value
 47        at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestValue').try(:text)
 48      end
 49
 50      def expected_digest_value
 51        digests = dsignature.references.map do |xxx|
 52          Base64.encode64(xxx.calculate_digest_value).chomp
 53        end
 54        digests.count > 1 ? digests : digests[0]
 55      end
 56
 57      def digest_method
 58        xpath = './ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm'
 59        at_xpath(xpath).try(:value)
 60      end
 61
 62      def signature_value
 63        at_xpath('./ds:SignatureValue').try(:text)
 64      end
 65
 66      def signature_method
 67        at_xpath('./ds:SignedInfo/ds:SignatureMethod/@Algorithm').try(:value)
 68      end
 69
 70      def canonicalization_method
 71        xpath = './ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm'
 72        at_xpath(xpath).try(:value)
 73      end
 74
 75      def transforms
 76        xpath = './ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform'
 77        node.search("#{xpath}/@Algorithm", Saml::Kit::Document::NAMESPACES)
 78          .try(:map, &:value)
 79      end
 80
 81      # Returns the XML Hash.
 82      def to_h
 83        @to_h ||= present? ? Hash.from_xml(to_xml)['Signature'] : {}
 84      end
 85
 86      def present?
 87        node.present?
 88      end
 89
 90      def to_xml(pretty: nil)
 91        pretty ? node.to_xml(indent: 2) : to_s
 92      end
 93
 94      def to_s
 95        node.to_s
 96      end
 97
 98      private
 99
100      attr_reader :node
101
102      def validate_signature
103        return errors.add(:base, error_message(:empty)) if certificate.nil?
104        return if dsignature.valid?(certificate.x509)
105
106        dsignature.errors.each do |attribute|
107          errors.add(attribute, error_message(attribute))
108        end
109      rescue StandardError => error
110        errors.add(:base, error.message)
111      end
112
113      def validate_reference
114        return unless present?
115        return if reference.bound?
116
117        errors.add(:reference, error_message(:reference))
118      end
119
120      def reference
121        @reference ||= SignatureReference.new(node)
122      end
123
124      def validate_certificate(now = Time.now.utc)
125        return unless certificate.present?
126        return if certificate.active?(now)
127
128        message = error_message(
129          :certificate,
130          not_before: certificate.not_before,
131          not_after: certificate.not_after
132        )
133        errors.add(:certificate, message)
134      end
135
136      def at_xpath(xpath)
137        return nil unless node
138
139        node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
140      end
141
142      def dsignature
143        @dsignature ||= Xmldsig::Signature.new(node, SignatureReference::ID_ATTR)
144      end
145    end
146  end
147end