main
 1# frozen_string_literal: true
 2
 3module Xml
 4  module Kit
 5    # {include:file:spec/xml/kit/document_spec.rb}
 6    class Document
 7      include ActiveModel::Validations
 8      NAMESPACES = { "ds": ::Xml::Kit::Namespaces::XMLDSIG }.freeze
 9
10      validate :validate_signatures
11      validate :validate_certificates
12
13      def initialize(raw_xml, namespaces: NAMESPACES)
14        @raw_xml = raw_xml
15        @namespaces = namespaces
16        @document = ::Nokogiri::XML(raw_xml)
17      end
18
19      # Returns the first XML node found by searching the document with the provided XPath.
20      #
21      # @param xpath [String] the XPath to use to search the document
22      def find_by(xpath)
23        document.at_xpath(xpath, namespaces)
24      end
25
26      # Returns all XML nodes found by searching the document with the provided XPath.
27      #
28      # @param xpath [String] the XPath to use to search the document
29      def find_all(xpath)
30        document.search(xpath, namespaces)
31      end
32
33      # Return the XML document as a [String].
34      #
35      # @param pretty [Boolean] return the XML string in a human readable format if true.
36      def to_xml(pretty: true)
37        pretty ? document.to_xml(indent: 2) : raw_xml
38      end
39
40      private
41
42      attr_reader :raw_xml, :document, :namespaces
43
44      def validate_signatures
45        invalid_signatures.flat_map(&:errors).uniq.each do |error|
46          errors.add(error, 'is invalid')
47        end
48      end
49
50      def invalid_signatures(id_attr: 'ID=$uri or @Id')
51        Xmldsig::SignedDocument
52          .new(document, id_attr: id_attr)
53          .signatures.find_all do |signature|
54          x509_certificates.all? do |certificate|
55            !signature.valid?(certificate)
56          end
57        end
58      end
59
60      def validate_certificates(now = Time.current)
61        return if find_by('//ds:Signature').nil?
62
63        x509_certificates.each do |certificate|
64          errors.add(:certificate, "Not valid before #{certificate.not_before}") if now < certificate.not_before
65
66          errors.add(:certificate, "Not valid after #{certificate.not_after}") if now > certificate.not_after
67        end
68      end
69
70      def x509_certificates
71        find_all('//ds:KeyInfo/ds:X509Data/ds:X509Certificate').map do |item|
72          Certificate.to_x509(item.text)
73        end
74      end
75    end
76  end
77end