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