moxml 0.1.25 → 0.2.0

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/.github/workflows/windows.yml +62 -0
  4. data/Gemfile +3 -1
  5. data/README.adoc +133 -6
  6. data/docs/signature/algorithms.md +108 -0
  7. data/docs/signature/architecture.md +148 -0
  8. data/docs/signature/c14n.md +110 -0
  9. data/docs/signature/examples.md +109 -0
  10. data/docs/signature/flows.md +132 -0
  11. data/docs/signature/quick-reference.md +115 -0
  12. data/docs/signature/security.md +143 -0
  13. data/examples/signature/auto_key_extraction.rb +24 -0
  14. data/examples/signature/enveloped_rsa.rb +45 -0
  15. data/examples/signature/hmac.rb +33 -0
  16. data/lib/compat/opal/moxml_boot.rb +5 -0
  17. data/lib/moxml/adapter/base.rb +53 -49
  18. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  19. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  20. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  21. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  22. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  23. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  24. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  25. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  26. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  27. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  28. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  29. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  30. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  31. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  32. data/lib/moxml/adapter/customized_oga.rb +2 -0
  33. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  34. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  35. data/lib/moxml/adapter/headed_ox.rb +9 -128
  36. data/lib/moxml/adapter/leptris.rb +965 -0
  37. data/lib/moxml/adapter/libxml.rb +131 -99
  38. data/lib/moxml/adapter/nokogiri.rb +29 -8
  39. data/lib/moxml/adapter/oga.rb +91 -54
  40. data/lib/moxml/adapter/ox.rb +93 -137
  41. data/lib/moxml/adapter/rexml.rb +61 -50
  42. data/lib/moxml/adapter.rb +2 -1
  43. data/lib/moxml/attribute.rb +8 -4
  44. data/lib/moxml/attribute_resolver.rb +189 -0
  45. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  46. data/lib/moxml/c14n/character_encoder.rb +29 -0
  47. data/lib/moxml/c14n/data_model.rb +145 -0
  48. data/lib/moxml/c14n/exclusive.rb +188 -0
  49. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  50. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  51. data/lib/moxml/c14n/namespace_context.rb +68 -0
  52. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  53. data/lib/moxml/c14n/node.rb +42 -0
  54. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  55. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  56. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  57. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  58. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  59. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  60. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  61. data/lib/moxml/c14n/nodes.rb +17 -0
  62. data/lib/moxml/c14n/processor.rb +118 -0
  63. data/lib/moxml/c14n/writer.rb +121 -0
  64. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  65. data/lib/moxml/c14n.rb +96 -0
  66. data/lib/moxml/config.rb +29 -3
  67. data/lib/moxml/context.rb +12 -0
  68. data/lib/moxml/element.rb +29 -12
  69. data/lib/moxml/entity/reference.rb +28 -0
  70. data/lib/moxml/entity.rb +125 -0
  71. data/lib/moxml/node.rb +90 -1
  72. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  73. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  74. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  75. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  76. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  77. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  78. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  79. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  80. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  81. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  82. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  83. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  84. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  85. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  86. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  87. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  88. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  89. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  90. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  91. data/lib/moxml/signature/algorithms.rb +113 -0
  92. data/lib/moxml/signature/errors.rb +99 -0
  93. data/lib/moxml/signature/key_extractor.rb +172 -0
  94. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  95. data/lib/moxml/signature/model/digest_method.rb +15 -0
  96. data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
  97. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  98. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  99. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  100. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  101. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  102. data/lib/moxml/signature/model/key_info.rb +21 -0
  103. data/lib/moxml/signature/model/key_value.rb +20 -0
  104. data/lib/moxml/signature/model/object_element.rb +18 -0
  105. data/lib/moxml/signature/model/reference.rb +22 -0
  106. data/lib/moxml/signature/model/signature.rb +21 -0
  107. data/lib/moxml/signature/model/signature_value.rb +16 -0
  108. data/lib/moxml/signature/model/signed_info.rb +20 -0
  109. data/lib/moxml/signature/model/transform.rb +16 -0
  110. data/lib/moxml/signature/model/transforms.rb +36 -0
  111. data/lib/moxml/signature/model.rb +35 -0
  112. data/lib/moxml/signature/parser.rb +270 -0
  113. data/lib/moxml/signature/reference_resolver.rb +79 -0
  114. data/lib/moxml/signature/reference_result.rb +22 -0
  115. data/lib/moxml/signature/serializer.rb +163 -0
  116. data/lib/moxml/signature/signer.rb +73 -0
  117. data/lib/moxml/signature/single_verification_result.rb +31 -0
  118. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  119. data/lib/moxml/signature/verification_result.rb +27 -0
  120. data/lib/moxml/signature/verifier.rb +129 -0
  121. data/lib/moxml/signature.rb +94 -0
  122. data/lib/moxml/version.rb +1 -1
  123. data/lib/moxml/xml_emitter.rb +71 -0
  124. data/lib/moxml/xpath/compiler.rb +60 -7
  125. data/lib/moxml/xpath/parser.rb +22 -15
  126. data/lib/moxml.rb +5 -0
  127. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  128. data/reference-docs/w3c-xmldsig-core.md +400 -0
  129. data/spec/examples/readme_examples_spec.rb +0 -4
  130. data/spec/examples/xpath_examples_spec.rb +0 -11
  131. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  132. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  133. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  134. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  135. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  136. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  137. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  138. data/spec/integration/all_adapters_spec.rb +17 -0
  139. data/spec/integration/sax_parity_spec.rb +58 -0
  140. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  141. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  142. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  143. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  144. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  145. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  146. data/spec/moxml/adapter/ox_spec.rb +20 -6
  147. data/spec/moxml/adapter/platform_spec.rb +15 -2
  148. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  149. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  150. data/spec/moxml/c14n/api_spec.rb +117 -0
  151. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  152. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  153. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  154. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  155. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  156. data/spec/moxml/doctype_spec.rb +1 -1
  157. data/spec/moxml/entity_spec.rb +76 -0
  158. data/spec/moxml/node_spec.rb +104 -0
  159. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  160. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  161. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  162. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  163. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  164. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  165. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  166. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  167. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  168. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  169. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  170. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  171. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  172. data/spec/moxml/signature/model_spec.rb +54 -0
  173. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  174. data/spec/moxml/xml_emitter_spec.rb +64 -0
  175. data/spec/moxml/xpath/axes_spec.rb +72 -0
  176. data/spec/moxml/xpath/parser_spec.rb +6 -0
  177. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  178. data/spec/performance/benchmark_spec.rb +13 -6
  179. data/spec/performance/thread_safety_spec.rb +0 -4
  180. data/spec/spec_helper.rb +5 -1
  181. metadata +130 -2
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Moxml
6
+ module Signature
7
+ # Translates a Moxml::Document containing ds:Signature into a Model::Signature.
8
+ #
9
+ # Robust against namespace prefix variations (ds:, dsig:, default ns).
10
+ class Parser
11
+ DS = { "ds" => DSIG_NS }.freeze
12
+
13
+ attr_reader :context
14
+
15
+ def initialize(context:)
16
+ @context = context
17
+ end
18
+
19
+ # `signature_element`: a Moxml::Element whose name is Signature in the
20
+ # xmldsig# namespace.
21
+ def parse(signature_element)
22
+ Model::Signature.new(
23
+ id: signature_element["Id"],
24
+ signed_info: parse_signed_info(at_child(signature_element, "SignedInfo")),
25
+ signature_value: parse_signature_value(at_child(signature_element, "SignatureValue")),
26
+ key_info: parse_key_info(at_child(signature_element, "KeyInfo")),
27
+ objects: [],
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def parse_signed_info(elem)
34
+ return nil unless elem
35
+
36
+ Model::SignedInfo.new(
37
+ id: elem["Id"],
38
+ canonicalization_method: parse_algorithm_method(at_child(elem, "CanonicalizationMethod")),
39
+ signature_method: parse_algorithm_method(at_child(elem, "SignatureMethod")),
40
+ references: at_children(elem, "Reference").map { |r| parse_reference(r) },
41
+ )
42
+ end
43
+
44
+ def parse_algorithm_method(elem)
45
+ return nil unless elem
46
+
47
+ parameters = {}
48
+ hmac_len = at_child(elem, "HMACOutputLength")
49
+ if hmac_len
50
+ length = begin
51
+ Integer(hmac_len.text.strip)
52
+ rescue StandardError
53
+ nil
54
+ end
55
+ parameters[:hmac_output_length] = length if length
56
+ end
57
+
58
+ Model::AlgorithmMethod.new(
59
+ algorithm: elem["Algorithm"],
60
+ parameters: parameters,
61
+ )
62
+ end
63
+
64
+ def parse_reference(elem)
65
+ return nil unless elem
66
+
67
+ transforms_elem = at_child(elem, "Transforms")
68
+ Model::Reference.new(
69
+ id: elem["Id"],
70
+ uri: elem["URI"],
71
+ type: elem["Type"],
72
+ transforms: parse_transforms(transforms_elem),
73
+ digest_method: parse_digest_method(at_child(elem, "DigestMethod")),
74
+ digest_value: at_child(elem, "DigestValue")&.text || "",
75
+ )
76
+ end
77
+
78
+ def parse_transforms(elem)
79
+ return nil unless elem
80
+
81
+ transforms = at_children(elem, "Transform").map do |t|
82
+ xpath_children = at_children(t, "XPath").map(&:text)
83
+ Model::Transform.new(
84
+ algorithm: t["Algorithm"],
85
+ parameters: xpath_children.empty? ? {} : { xpaths: xpath_children },
86
+ )
87
+ end
88
+ Model::Transforms.new(transforms: transforms)
89
+ end
90
+
91
+ def parse_digest_method(elem)
92
+ return nil unless elem
93
+
94
+ Model::DigestMethod.new(algorithm: elem["Algorithm"])
95
+ end
96
+
97
+ def parse_signature_value(elem)
98
+ return nil unless elem
99
+
100
+ text = (elem.text || "").gsub(/\s+/, "")
101
+ value = text.empty? ? nil : Base64.strict_decode64(text)
102
+ Model::SignatureValue.new(id: elem["Id"], value: value)
103
+ rescue ArgumentError => e
104
+ raise MalformedSignatureError.new(
105
+ "SignatureValue is not valid base64: #{e.message}",
106
+ )
107
+ end
108
+
109
+ def parse_key_info(elem)
110
+ return nil unless elem
111
+
112
+ key_name_el = at_child(elem, "KeyName")
113
+ x509_data_el = at_child(elem, "X509Data")
114
+ key_value_el = at_child(elem, "KeyValue")
115
+
116
+ Model::KeyInfo.new(
117
+ id: elem["Id"],
118
+ key_name: key_name_el&.text,
119
+ x509_data: parse_x509_data(x509_data_el),
120
+ key_value: parse_key_value(key_value_el),
121
+ raw_elements: raw_key_info_children(elem),
122
+ )
123
+ end
124
+
125
+ def parse_x509_data(elem)
126
+ return nil unless elem
127
+
128
+ certificates = at_children(elem, "X509Certificate").map do |cert_el|
129
+ strip_base64(cert_el.text)
130
+ end
131
+ issuer_serial_el = at_child(elem, "X509IssuerSerial")
132
+ subject_name_el = at_child(elem, "X509SubjectName")
133
+ ski_el = at_child(elem, "X509SKI")
134
+ crl_els = at_children(elem, "X509CRL")
135
+ # dsig11:X509Digest is in a different namespace; look it up loosely.
136
+ digest_els = elem.children.select do |c|
137
+ c.is_a?(::Moxml::Element) &&
138
+ c.name == "X509Digest" &&
139
+ c.namespace_uri == DSIG11_NS
140
+ end
141
+
142
+ Model::Key::X509Data.new(
143
+ issuer_serial: parse_x509_issuer_serial(issuer_serial_el),
144
+ subject_name: subject_name_el&.text,
145
+ subject_key_id: strip_base64(ski_el&.text),
146
+ certificates: certificates,
147
+ crls: crl_els.map { |e| strip_base64(e.text) },
148
+ digests: digest_els.map { |e| parse_x509_digest(e) },
149
+ )
150
+ end
151
+
152
+ def parse_x509_issuer_serial(elem)
153
+ return nil unless elem
154
+
155
+ issuer = at_child(elem, "X509IssuerName")
156
+ serial = at_child(elem, "X509SerialNumber")
157
+ return nil unless issuer && serial
158
+
159
+ Model::Key::X509IssuerSerial.new(
160
+ issuer_name: issuer.text,
161
+ serial_number: serial.text,
162
+ )
163
+ end
164
+
165
+ def parse_x509_digest(elem)
166
+ return nil unless elem
167
+
168
+ Model::Key::X509Digest.new(
169
+ algorithm: elem["Algorithm"],
170
+ digest: strip_base64(elem.text),
171
+ )
172
+ end
173
+
174
+ def parse_key_value(elem)
175
+ return nil unless elem
176
+
177
+ rsa_el = at_child(elem, "RSAKeyValue")
178
+ dsa_el = at_child(elem, "DSAKeyValue")
179
+ ec_el = elem.children.find do |c|
180
+ c.is_a?(::Moxml::Element) &&
181
+ c.name == "ECKeyValue" &&
182
+ c.namespace_uri == DSIG11_NS
183
+ end
184
+
185
+ Model::KeyValue.new(
186
+ rsa_key_value: parse_rsa_key_value(rsa_el),
187
+ dsa_key_value: parse_dsa_key_value(dsa_el),
188
+ ec_key_value: parse_ec_key_value(ec_el),
189
+ )
190
+ end
191
+
192
+ def parse_rsa_key_value(elem)
193
+ return nil unless elem
194
+
195
+ Model::Key::RSAKeyValue.new(
196
+ modulus: text_of(elem, "Modulus"),
197
+ exponent: text_of(elem, "Exponent"),
198
+ )
199
+ end
200
+
201
+ def parse_dsa_key_value(elem)
202
+ return nil unless elem
203
+
204
+ Model::Key::DSAKeyValue.new(
205
+ p: text_of(elem, "P"),
206
+ q: text_of(elem, "Q"),
207
+ g: text_of(elem, "G"),
208
+ y: text_of(elem, "Y"),
209
+ j: text_of(elem, "J"),
210
+ seed: text_of(elem, "Seed"),
211
+ pgen_counter: text_of(elem, "PgenCounter"),
212
+ )
213
+ end
214
+
215
+ def parse_ec_key_value(elem)
216
+ return nil unless elem
217
+
218
+ named_curve_el = elem.children.find do |c|
219
+ c.is_a?(::Moxml::Element) &&
220
+ c.name == "NamedCurve" &&
221
+ c.namespace_uri == DSIG11_NS
222
+ end
223
+ public_key_el = elem.children.find do |c|
224
+ c.is_a?(::Moxml::Element) &&
225
+ c.name == "PublicKey" &&
226
+ c.namespace_uri == DSIG11_NS
227
+ end
228
+
229
+ Model::Key::ECKeyValue.new(
230
+ named_curve_uri: named_curve_el&.[]("URI"),
231
+ public_key: strip_base64(public_key_el&.text),
232
+ )
233
+ end
234
+
235
+ def text_of(parent, local_name)
236
+ elem = at_child(parent, local_name)
237
+ elem&.text
238
+ end
239
+
240
+ def strip_base64(text)
241
+ return nil if text.nil?
242
+
243
+ text.to_s.gsub(/\s+/, "")
244
+ end
245
+
246
+ def raw_key_info_children(elem)
247
+ # Children we don't model explicitly are preserved for round-trip
248
+ # fidelity (PGPData, SPKIData, MgmtData, dsig11:KeyInfoReference,
249
+ # dsig11:DEREncodedKeyValue, xenc:*).
250
+ elem.children.grep(::Moxml::Element).to_a
251
+ end
252
+
253
+ def at_child(parent, local_name)
254
+ parent.children.find do |c|
255
+ c.is_a?(::Moxml::Element) &&
256
+ c.name == local_name &&
257
+ c.namespace_uri == DSIG_NS
258
+ end
259
+ end
260
+
261
+ def at_children(parent, local_name)
262
+ parent.children.select do |c|
263
+ c.is_a?(::Moxml::Element) &&
264
+ c.name == local_name &&
265
+ c.namespace_uri == DSIG_NS
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Translates a Reference URI to a node (subtree) or octet string ready
6
+ # for the transform pipeline.
7
+ #
8
+ # Only same-document references and bare-octet inputs are handled in
9
+ # this tier. External URI dereferencing is documented in
10
+ # TODO.complete/11.
11
+ class ReferenceResolver
12
+ attr_reader :context, :document
13
+
14
+ def initialize(context:, document:)
15
+ @context = context
16
+ @document = document
17
+ end
18
+
19
+ # Returns:
20
+ # - Moxml::Node for same-document references (subtree apex)
21
+ # - String (octets) for octet inputs
22
+ def resolve(uri)
23
+ case uri.to_s
24
+ when ""
25
+ document.root
26
+ when /\A#/
27
+ resolve_fragment(uri[1..])
28
+ else
29
+ uri.to_s
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def resolve_fragment(fragment)
36
+ case fragment
37
+ when /\Axpointer\(\/\)\z/
38
+ document.root
39
+ when /\Axpointer\(id\(['"]([^'"]+)['"]\)\)\z/
40
+ find_by_id(::Regexp.last_match(1))
41
+ else
42
+ find_by_id(fragment)
43
+ end
44
+ end
45
+
46
+ def find_by_id(id)
47
+ candidate = search_by_xml_id(id) || search_by_attribute_id(id)
48
+ unless candidate
49
+ raise MalformedSignatureError.new(
50
+ "Reference URI ##{id.inspect} does not resolve to any element",
51
+ )
52
+ end
53
+ candidate
54
+ end
55
+
56
+ def search_by_xml_id(id)
57
+ result = document.at_xpath("//*[@xml:id='#{xpath_escape(id)}']")
58
+ return nil unless result
59
+ return nil unless result.is_a?(::Moxml::Element)
60
+
61
+ result
62
+ end
63
+
64
+ def search_by_attribute_id(id)
65
+ matches = document
66
+ .xpath("//*[@Id='#{xpath_escape(id)}'] | " \
67
+ "//*[@ID='#{xpath_escape(id)}']")
68
+ .grep(::Moxml::Element)
69
+ return nil if matches.empty?
70
+
71
+ matches.first
72
+ end
73
+
74
+ def xpath_escape(value)
75
+ value.to_s.gsub("'", "''")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Per-reference digest comparison result. `valid?` is true iff the
6
+ # freshly-computed digest matches the expected DigestValue.
7
+ class ReferenceResult
8
+ attr_reader :uri, :digest_method, :expected, :computed
9
+
10
+ def initialize(uri:, digest_method:, expected:, computed:)
11
+ @uri = uri
12
+ @digest_method = digest_method
13
+ @expected = expected
14
+ @computed = computed
15
+ end
16
+
17
+ def valid?
18
+ expected == computed
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Moxml
6
+ module Signature
7
+ # Translates a Model::Signature into a Moxml::Document.
8
+ #
9
+ # All XML construction goes through moxml primitives
10
+ # (create_element, []=, add_child, create_text). No string templates,
11
+ # no manual attribute escaping — moxml handles entity encoding
12
+ # correctly per the XML spec.
13
+ class Serializer
14
+ DS_PREFIX = "ds"
15
+
16
+ attr_reader :context
17
+
18
+ def initialize(context:)
19
+ @context = context
20
+ end
21
+
22
+ def serialize(signature)
23
+ doc = context.create_document
24
+ root = create_namespaced_element("Signature", doc)
25
+ root["Id"] = signature.id if signature.id
26
+ doc.root = root
27
+
28
+ if signature.signed_info
29
+ root.add_child(build_signed_info(signature.signed_info, doc))
30
+ end
31
+ if signature.signature_value
32
+ root.add_child(build_signature_value(signature.signature_value, doc))
33
+ end
34
+ if signature.key_info
35
+ root.add_child(build_key_info(signature.key_info, doc))
36
+ end
37
+ signature.objects.each do |obj|
38
+ root.add_child(build_object(obj, doc))
39
+ end
40
+
41
+ doc
42
+ end
43
+
44
+ # Produces a standalone ds:SignedInfo document. Used by the Signer
45
+ # when canonicalizing SignedInfo in isolation.
46
+ def serialize_signed_info(signed_info)
47
+ doc = context.create_document
48
+ root = create_namespaced_element("SignedInfo", doc)
49
+ root["Id"] = signed_info.id if signed_info.id
50
+ doc.root = root
51
+ populate_signed_info(root, signed_info, doc)
52
+ doc
53
+ end
54
+
55
+ private
56
+
57
+ def build_signed_info(signed_info, doc)
58
+ el = create_namespaced_element("SignedInfo", doc)
59
+ el["Id"] = signed_info.id if signed_info.id
60
+ populate_signed_info(el, signed_info, doc)
61
+ el
62
+ end
63
+
64
+ def populate_signed_info(root, signed_info, doc)
65
+ if signed_info.canonicalization_method
66
+ root.add_child(
67
+ build_algorithm_method("CanonicalizationMethod",
68
+ signed_info.canonicalization_method, doc),
69
+ )
70
+ end
71
+ if signed_info.signature_method
72
+ root.add_child(
73
+ build_algorithm_method("SignatureMethod",
74
+ signed_info.signature_method, doc),
75
+ )
76
+ end
77
+ signed_info.references.each do |ref|
78
+ root.add_child(build_reference(ref, doc))
79
+ end
80
+ end
81
+
82
+ def build_algorithm_method(name, method, doc)
83
+ el = create_namespaced_element(name, doc)
84
+ el["Algorithm"] = method.algorithm
85
+ if method.parameters.is_a?(Hash) && method.parameters[:hmac_output_length]
86
+ len = create_namespaced_element("HMACOutputLength", doc)
87
+ len.add_child(doc.create_text(method.parameters[:hmac_output_length].to_s))
88
+ el.add_child(len)
89
+ end
90
+ el
91
+ end
92
+
93
+ def build_reference(reference, doc)
94
+ el = create_namespaced_element("Reference", doc)
95
+ el["Id"] = reference.id if reference.id
96
+ el["URI"] = reference.uri if reference.uri
97
+ el["Type"] = reference.type if reference.type
98
+
99
+ if reference.transforms && !reference.transforms.empty?
100
+ tf = create_namespaced_element("Transforms", doc)
101
+ reference.transforms.each do |transform|
102
+ tr = create_namespaced_element("Transform", doc)
103
+ tr["Algorithm"] = transform.algorithm
104
+ tf.add_child(tr)
105
+ end
106
+ el.add_child(tf)
107
+ end
108
+
109
+ if reference.digest_method
110
+ dm = create_namespaced_element("DigestMethod", doc)
111
+ dm["Algorithm"] = reference.digest_method.algorithm
112
+ el.add_child(dm)
113
+ end
114
+
115
+ dv = create_namespaced_element("DigestValue", doc)
116
+ dv.add_child(doc.create_text(reference.digest_value || ""))
117
+ el.add_child(dv)
118
+ el
119
+ end
120
+
121
+ def build_signature_value(signature_value, doc)
122
+ el = create_namespaced_element("SignatureValue", doc)
123
+ el["Id"] = signature_value.id if signature_value.id
124
+ if signature_value.value
125
+ el.add_child(doc.create_text(Base64.strict_encode64(signature_value.value)))
126
+ end
127
+ el
128
+ end
129
+
130
+ def build_key_info(key_info, doc)
131
+ el = create_namespaced_element("KeyInfo", doc)
132
+ el["Id"] = key_info.id if key_info.id
133
+ if key_info.key_name
134
+ kn = create_namespaced_element("KeyName", doc)
135
+ kn.add_child(doc.create_text(key_info.key_name))
136
+ el.add_child(kn)
137
+ end
138
+ key_info.raw_elements.each { |raw| el.add_child(raw.dup) }
139
+ el
140
+ end
141
+
142
+ def build_object(obj, doc)
143
+ el = create_namespaced_element("Object", doc)
144
+ el["Id"] = obj.id if obj.id
145
+ el["MimeType"] = obj.mime_type if obj.mime_type
146
+ el["Encoding"] = obj.encoding if obj.encoding
147
+ # Payload content (Manifest, SignatureProperties, XAdES
148
+ # QualifyingProperties) comes in later tiers.
149
+ el
150
+ end
151
+
152
+ # Create an element in the xmldsig# namespace with the conventional
153
+ # `ds` prefix. The namespace declaration is added once on the
154
+ # element itself; descendants inherit it.
155
+ def create_namespaced_element(local_name, doc)
156
+ el = doc.create_element(local_name)
157
+ el.add_namespace(DS_PREFIX, DSIG_NS)
158
+ el.namespace = { DS_PREFIX => DSIG_NS }
159
+ el
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Reference generation + SignatureValue computation per spec §3.1.
6
+ #
7
+ # Flow:
8
+ # 1. For each Reference in signed_info.references:
9
+ # a. Resolve URI (same-document or octet).
10
+ # b. Apply each Transform in order.
11
+ # c. Compute digest via DigestMethod.
12
+ # d. Store base64 digest on Reference#digest_value.
13
+ # 2. Serialize SignedInfo.
14
+ # 3. Canonicalize per CanonicalizationMethod.
15
+ # 4. Sign via SignatureMethod#sign(canonical_octets, key).
16
+ # 5. Set signature.signature_value.
17
+ class Signer
18
+ attr_reader :context, :signature, :document, :key
19
+
20
+ def initialize(context:, signature:, document:, key:)
21
+ @context = context
22
+ @signature = signature
23
+ @document = document
24
+ @key = key
25
+ end
26
+
27
+ def sign
28
+ Algorithms.load_builtins!
29
+
30
+ signature.signed_info.references.each do |ref|
31
+ digest = compute_reference_digest(ref)
32
+ ref.digest_value = digest
33
+ end
34
+
35
+ signature.signature_value = Model::SignatureValue.new(
36
+ value: compute_signature_value,
37
+ )
38
+ signature
39
+ end
40
+
41
+ private
42
+
43
+ def compute_reference_digest(reference)
44
+ resolver = ReferenceResolver.new(context: context, document: document)
45
+ input = resolver.resolve(reference.uri)
46
+ pipeline = TransformPipeline.new(context: context, signature_element: nil)
47
+ transformed = pipeline.apply(input, reference.transforms)
48
+ canonical = pipeline.to_octets(transformed, reference)
49
+ digest_algo = Algorithms.lookup(:digest, reference.digest_method.algorithm).new
50
+ digest_algo.digest_base64(canonical)
51
+ end
52
+
53
+ def compute_signature_value
54
+ signed_info = signature.signed_info
55
+ serializer = Serializer.new(context: context)
56
+ signed_info_doc = serializer.serialize_signed_info(signed_info)
57
+
58
+ c14n_uri = signed_info.canonicalization_method.algorithm
59
+ c14n_klass = Algorithms.lookup(:canonicalization, c14n_uri)
60
+ c14n = c14n_klass.new(identifier_uri: c14n_uri)
61
+ canonical_octets = c14n.canonicalize(signed_info_doc.root)
62
+
63
+ sm_uri = signed_info.signature_method.algorithm
64
+ sm_klass = Algorithms.lookup(:signature_method, sm_uri)
65
+ sm = sm_klass.new(
66
+ identifier_uri: sm_uri,
67
+ parameters: signed_info.signature_method.parameters,
68
+ )
69
+ sm.sign(canonical_octets, key)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Verification outcome for a single Signature element. Carries the
6
+ # cryptographic-result boolean, per-reference results, and (when
7
+ # applicable) the error that caused failure.
8
+ class SingleVerificationResult
9
+ attr_reader :signature_id, :references, :error
10
+
11
+ def initialize(signature_id:, signature_valid:, references:, error: nil)
12
+ @signature_id = signature_id
13
+ @signature_valid = signature_valid
14
+ @references = references
15
+ @error = error
16
+ end
17
+
18
+ def signature_valid?
19
+ @signature_valid
20
+ end
21
+
22
+ def valid?
23
+ @signature_valid && references.all?(&:valid?)
24
+ end
25
+
26
+ def failing_references
27
+ references.reject(&:valid?)
28
+ end
29
+ end
30
+ end
31
+ end