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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +130 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Pins cross-adapter parity for entity-decoding in attribute values and
|
|
6
|
+
# text content, across SAX, DOM, and build-then-serialize-then-reparse.
|
|
7
|
+
# Previously the four adapters diverged: nokogiri/libxml SAX failed to
|
|
8
|
+
# resolve "&#NN;"; rexml SAX delivered raw escaped strings; oga DOM
|
|
9
|
+
# and SAX double-decoded.
|
|
10
|
+
|
|
11
|
+
# Namespace-isolated container for the test data and the SAX capture
|
|
12
|
+
# handler. Wrapping these in a module rather than declaring them as
|
|
13
|
+
# top-level constants inside `RSpec.describe` keeps them out of the
|
|
14
|
+
# global namespace and lets the spec reference them without tripping
|
|
15
|
+
# the RSpec/* cops that forbid local-variable use inside examples.
|
|
16
|
+
module SaxEntityParityFixtures
|
|
17
|
+
class CaptureHandler < Moxml::SAX::ElementHandler
|
|
18
|
+
attr_reader :first_attrs, :text
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
super
|
|
22
|
+
@first_attrs = nil
|
|
23
|
+
@text = +""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_start_element(_name, attrs = {}, _namespaces = {})
|
|
27
|
+
@first_attrs = attrs.dup if @first_attrs.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_characters(chunk)
|
|
31
|
+
@text << chunk
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
ADAPTERS = %i[nokogiri ox oga rexml libxml headed_ox].freeze
|
|
36
|
+
|
|
37
|
+
# Each row: [input XML, decoded attribute value, expected attribute
|
|
38
|
+
# value as it appears in the serialized XML]. The serialized form
|
|
39
|
+
# asserts what every adapter must emit byte-for-byte for the attribute.
|
|
40
|
+
ATTRIBUTE_CASES = {
|
|
41
|
+
"wrapped amp + decimal ref" => ['<doc x="&#38;"/>', "&", "&#38;"],
|
|
42
|
+
"wrapped amp + hex ref" => ['<doc x="&#x26;"/>', "&", "&#x26;"],
|
|
43
|
+
"wrapped amp + non-std ref" => ['<doc x="&copy;"/>', "©", "&copy;"],
|
|
44
|
+
"wrapped amp + lt" => ['<doc x="&lt;"/>', "<", "&lt;"],
|
|
45
|
+
"wrapped amp + amp" => ['<doc x="&amp;"/>', "&", "&amp;"],
|
|
46
|
+
"two amps" => ['<doc x="&&"/>', "&&", "&&"],
|
|
47
|
+
"plain amp" => ['<doc x="&"/>', "&", "&"],
|
|
48
|
+
"plain decimal ref" => ['<doc x="&"/>', "&", "&"],
|
|
49
|
+
"plain hex ref" => ['<doc x="&"/>', "&", "&"],
|
|
50
|
+
"high codepoint" => ['<doc x="©"/>', "©", "©"],
|
|
51
|
+
"no entities" => ['<doc x="plain"/>', "plain", "plain"],
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Each row: [input XML, decoded text, expected text as it appears in
|
|
55
|
+
# serialized XML].
|
|
56
|
+
TEXT_CASES = {
|
|
57
|
+
"text wrapped amp + decimal" => ["<doc>&#38;</doc>", "&", "&#38;"],
|
|
58
|
+
"text wrapped amp + non-std" => ["<doc>&copy;</doc>", "©", "&copy;"],
|
|
59
|
+
"text plain amp" => ["<doc>&</doc>", "&", "&"],
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# XML 1.0 §2.6 — entity references inside PI content are NOT resolved.
|
|
63
|
+
# Every adapter must surface the literal source text. The libxml
|
|
64
|
+
# adapter previously decoded these five entities via a gsub chain in
|
|
65
|
+
# processing_instruction_content; pin each one explicitly.
|
|
66
|
+
PI_ENTITY_CASES = {
|
|
67
|
+
"&" => "data & more",
|
|
68
|
+
"<" => "data < more",
|
|
69
|
+
">" => "data > more",
|
|
70
|
+
""" => "data " more",
|
|
71
|
+
"'" => "data ' more",
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
# Per-adapter overrides for the rebuild-path serialized form. Oga's
|
|
75
|
+
# set_attribute calls preprocess_entities which marks non-standard
|
|
76
|
+
# named entities so they survive serialization as "&name;" rather than
|
|
77
|
+
# being escaped to "&name;". This is the entity-preservation
|
|
78
|
+
# feature documented in spec/moxml/adapter/entity_restoration_spec.rb
|
|
79
|
+
# and spec/moxml/adapter/oga_spec.rb — not a bug.
|
|
80
|
+
REBUILD_SERIALIZED_OVERRIDES = {
|
|
81
|
+
oga: {
|
|
82
|
+
"wrapped amp + non-std ref" => "©",
|
|
83
|
+
},
|
|
84
|
+
}.freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
RSpec.describe "SAX/DOM entity parity" do
|
|
88
|
+
SaxEntityParityFixtures::ADAPTERS.each do |adapter|
|
|
89
|
+
context "with #{adapter} adapter" do
|
|
90
|
+
let(:ctx) { Moxml.new(adapter) }
|
|
91
|
+
|
|
92
|
+
SaxEntityParityFixtures::ATTRIBUTE_CASES.each do |label, (xml, expected, expected_serialized)|
|
|
93
|
+
it "decodes attribute via SAX: #{label}" do
|
|
94
|
+
handler = SaxEntityParityFixtures::CaptureHandler.new
|
|
95
|
+
ctx.sax_parse(xml, handler)
|
|
96
|
+
expect(handler.first_attrs["x"]).to eq(expected)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "decodes attribute via DOM: #{label}" do
|
|
100
|
+
expect(ctx.parse(xml).root["x"]).to eq(expected)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "serializes attribute to expected XML form: #{label}" do
|
|
104
|
+
# End-to-end: parse → DOM access → set on fresh doc → serialize
|
|
105
|
+
# → assert exact serialized form. This pins what the consumer
|
|
106
|
+
# sees on the wire, not just what the parser delivers in memory.
|
|
107
|
+
parsed_value = ctx.parse(xml).root["x"]
|
|
108
|
+
|
|
109
|
+
doc2 = ctx.create_document
|
|
110
|
+
el = doc2.create_element("doc")
|
|
111
|
+
el["x"] = parsed_value
|
|
112
|
+
doc2.add_child(el)
|
|
113
|
+
|
|
114
|
+
serialized = doc2.to_xml.sub(/\A<\?[^>]*\?>\s*/, "")
|
|
115
|
+
expected_form = SaxEntityParityFixtures::REBUILD_SERIALIZED_OVERRIDES.dig(adapter, label) || expected_serialized
|
|
116
|
+
expect(serialized).to include(%(x="#{expected_form}"))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "round-trips attribute through build+serialize+reparse: #{label}" do
|
|
120
|
+
parsed_value = ctx.parse(xml).root["x"]
|
|
121
|
+
|
|
122
|
+
doc2 = ctx.create_document
|
|
123
|
+
el = doc2.create_element("doc")
|
|
124
|
+
el["x"] = parsed_value
|
|
125
|
+
doc2.add_child(el)
|
|
126
|
+
|
|
127
|
+
serialized = doc2.to_xml.sub(/\A<\?[^>]*\?>/, "")
|
|
128
|
+
expect(ctx.parse(serialized).root["x"]).to eq(parsed_value)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "re-serializes parsed document idempotently: #{label}" do
|
|
132
|
+
# Parse → serialize → re-parse → re-serialize → assert the two
|
|
133
|
+
# serializations match. This catches any non-idempotent
|
|
134
|
+
# mutations the adapter applies during the round-trip.
|
|
135
|
+
first = ctx.parse(xml).to_xml.sub(/\A<\?[^>]*\?>\s*/, "")
|
|
136
|
+
second = ctx.parse(first).to_xml.sub(/\A<\?[^>]*\?>\s*/, "")
|
|
137
|
+
expect(second).to eq(first)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
SaxEntityParityFixtures::TEXT_CASES.each do |label, (xml, expected, expected_serialized)|
|
|
142
|
+
it "decodes text via SAX: #{label}" do
|
|
143
|
+
handler = SaxEntityParityFixtures::CaptureHandler.new
|
|
144
|
+
ctx.sax_parse(xml, handler)
|
|
145
|
+
expect(handler.text).to eq(expected)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "decodes text via DOM: #{label}" do
|
|
149
|
+
expect(ctx.parse(xml).root.text).to eq(expected)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "serializes text to expected XML form: #{label}" do
|
|
153
|
+
serialized = ctx.parse(xml).to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
154
|
+
expect(serialized).to include(">#{expected_serialized}<")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "does not double-escape '&' in a programmatically-built text node" do
|
|
159
|
+
# Pins the libxml customized Text#to_xml fix: an authored "&"
|
|
160
|
+
# must serialize to "&" exactly once, never "&amp;".
|
|
161
|
+
# Regression target for adapter/customized_libxml/text.rb where
|
|
162
|
+
# the previous implementation used #content (decoded text) and
|
|
163
|
+
# lost the "&" → "&" escape, and earlier variants risked
|
|
164
|
+
# double-escaping on round-trip.
|
|
165
|
+
doc = ctx.create_document
|
|
166
|
+
el = doc.create_element("doc")
|
|
167
|
+
el.add_child("a & b")
|
|
168
|
+
doc.add_child(el)
|
|
169
|
+
|
|
170
|
+
serialized = doc.to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
171
|
+
expect(serialized).to include(">a & b<")
|
|
172
|
+
expect(serialized).not_to include("&amp;")
|
|
173
|
+
|
|
174
|
+
# And the round-trip must preserve the original text exactly.
|
|
175
|
+
reparsed_text = ctx.parse(serialized).root.text
|
|
176
|
+
expect(reparsed_text).to eq("a & b")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "mutates in-tree text node content without double-escaping '&'" do
|
|
180
|
+
# Pins the libxml fix for set_text_content on attached text
|
|
181
|
+
# nodes: libxml-ruby's #content= setter pre-escapes the input,
|
|
182
|
+
# and #to_s escapes again on serialization. The adapter must
|
|
183
|
+
# replace the in-tree node with a fresh raw-storage node to
|
|
184
|
+
# avoid silent double-escape data corruption.
|
|
185
|
+
doc = ctx.create_document
|
|
186
|
+
el = doc.create_element("doc")
|
|
187
|
+
text = doc.create_text("placeholder")
|
|
188
|
+
el.add_child(text)
|
|
189
|
+
doc.add_child(el)
|
|
190
|
+
|
|
191
|
+
text.content = "a & b"
|
|
192
|
+
expect(text.content).to eq("a & b")
|
|
193
|
+
|
|
194
|
+
serialized = doc.to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
195
|
+
expect(serialized).to include(">a & b<")
|
|
196
|
+
expect(serialized).not_to include("&amp;")
|
|
197
|
+
expect(ctx.parse(serialized).root.text).to eq("a & b")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "mutates in-tree PI content verbatim without escaping" do
|
|
201
|
+
# Pins the libxml fix for set_processing_instruction_content
|
|
202
|
+
# on attached PI nodes: libxml-ruby's #content= setter pre-
|
|
203
|
+
# escapes quotes and ampersands, but XML 1.0 §2.6 specifies
|
|
204
|
+
# PI content is verbatim — entity references are not resolved
|
|
205
|
+
# and no escaping is required on serialization.
|
|
206
|
+
doc = ctx.create_document
|
|
207
|
+
el = doc.create_element("doc")
|
|
208
|
+
pi = doc.create_processing_instruction("target", "placeholder")
|
|
209
|
+
el.add_child(pi)
|
|
210
|
+
doc.add_child(el)
|
|
211
|
+
|
|
212
|
+
pi.content = 'a " & b'
|
|
213
|
+
expect(pi.content.strip).to eq('a " & b')
|
|
214
|
+
|
|
215
|
+
serialized = doc.to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
216
|
+
expect(serialized).to include('a " & b?>')
|
|
217
|
+
expect(serialized).not_to include(""")
|
|
218
|
+
expect(serialized).not_to include("&")
|
|
219
|
+
reparsed = ctx.parse(serialized).root.children.find do |c|
|
|
220
|
+
c.is_a?(Moxml::ProcessingInstruction)
|
|
221
|
+
end
|
|
222
|
+
expect(reparsed.content.strip).to eq('a " & b')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "mutates unparented text/PI content before adoption into the tree" do
|
|
226
|
+
# Pins the swap_native_in_place early-return when parent is nil:
|
|
227
|
+
# set_text_content / set_processing_instruction_content called
|
|
228
|
+
# before the node is added to any element must still replace the
|
|
229
|
+
# wrapper's @native with a fresh raw-storage node, so the value
|
|
230
|
+
# survives correctly once the node is later attached and serialized.
|
|
231
|
+
doc = ctx.create_document
|
|
232
|
+
el = doc.create_element("doc")
|
|
233
|
+
|
|
234
|
+
text = doc.create_text("placeholder")
|
|
235
|
+
text.content = "a & b"
|
|
236
|
+
pi = doc.create_processing_instruction("target", "placeholder")
|
|
237
|
+
pi.content = 'a " & b'
|
|
238
|
+
|
|
239
|
+
el.add_child(text)
|
|
240
|
+
el.add_child(pi)
|
|
241
|
+
doc.add_child(el)
|
|
242
|
+
|
|
243
|
+
serialized = doc.to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
244
|
+
expect(serialized).to include(">a & b<")
|
|
245
|
+
expect(serialized).to include('<?target a " & b?>')
|
|
246
|
+
expect(serialized).not_to include("&amp;")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Regressions around & appearing inside contexts that Oga's
|
|
250
|
+
# source-level preprocessor must not touch (CDATA, comments,
|
|
251
|
+
# DOCTYPE) — verified across adapters to ensure parity holds.
|
|
252
|
+
it "preserves literal & inside CDATA" do
|
|
253
|
+
doc = ctx.parse("<doc><![CDATA[a & b]]></doc>")
|
|
254
|
+
expect(doc.root.text).to eq("a & b")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "preserves literal & inside comment" do
|
|
258
|
+
doc = ctx.parse("<doc><!-- a & b --></doc>")
|
|
259
|
+
comment = doc.root.children.find { |c| c.is_a?(Moxml::Comment) }
|
|
260
|
+
expect(comment.content.strip).to eq("a & b")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "preserves literal U+FFFC U+FFFC user data in attribute" do
|
|
264
|
+
xml = "<doc x=\"\u{FFFC}\u{FFFC} test\"/>"
|
|
265
|
+
expect(ctx.parse(xml).root["x"]).to eq("\u{FFFC}\u{FFFC} test")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "preserves literal U+FDD0 U+FDD0 user data in attribute" do
|
|
269
|
+
# Single noncharacters near the Oga marker codepoints — must not
|
|
270
|
+
# collide with the internal DEFAULT_AMP_MARKER sentinel.
|
|
271
|
+
xml = "<doc x=\"\u{FDD0}\u{FDD0} test\"/>"
|
|
272
|
+
expect(ctx.parse(xml).root["x"]).to eq("\u{FDD0}\u{FDD0} test")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it "preserves the exact Oga DEFAULT_AMP_MARKER sequence in attribute (no amp)" do
|
|
276
|
+
# User data matching the default Oga marker codepoints with NO
|
|
277
|
+
# & in the input — restore must not run, so the sequence
|
|
278
|
+
# survives intact.
|
|
279
|
+
xml = "<doc x=\"\u{FDD0}\u{FDD1}\u{FDD2} plain\"/>"
|
|
280
|
+
expect(ctx.parse(xml).root["x"]).to eq("\u{FDD0}\u{FDD1}\u{FDD2} plain")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "preserves the exact Oga DEFAULT_AMP_MARKER sequence in attribute (with amp)" do
|
|
284
|
+
# User data matching the default Oga marker codepoints alongside
|
|
285
|
+
# an & that the preprocessor would otherwise substitute.
|
|
286
|
+
# The adapter must pick a marker not present in user data so
|
|
287
|
+
# restore doesn't corrupt the original sequence.
|
|
288
|
+
xml = "<doc x=\"\u{FDD0}\u{FDD1}\u{FDD2} & foo\"/>"
|
|
289
|
+
expect(ctx.parse(xml).root["x"]).to eq("\u{FDD0}\u{FDD1}\u{FDD2} & foo")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "preserves the exact Oga DEFAULT_AMP_MARKER sequence in text" do
|
|
293
|
+
xml = "<doc>\u{FDD0}\u{FDD1}\u{FDD2} & foo</doc>"
|
|
294
|
+
expect(ctx.parse(xml).root.text).to eq("\u{FDD0}\u{FDD1}\u{FDD2} & foo")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it "handles DOCTYPE with '[' in quoted system ID" do
|
|
298
|
+
# System ID containing a literal "[" before the internal subset
|
|
299
|
+
# opener — quote-aware DOCTYPE terminator must not pick "]>"
|
|
300
|
+
# based on the bracket inside the quoted string.
|
|
301
|
+
xml = %(<!DOCTYPE root SYSTEM "has[bracket"><root x="&#38;"/>)
|
|
302
|
+
expect(ctx.parse(xml).root["x"]).to eq("&")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "handles bare DOCTYPE with '>' in quoted system ID" do
|
|
306
|
+
# Bare DOCTYPE (no internal subset) — the terminator is ">".
|
|
307
|
+
# A literal ">" inside the quoted SYSTEM id must not be treated
|
|
308
|
+
# as the end of the declaration. For the Oga adapter this also
|
|
309
|
+
# exercises find_block_terminator's quote-aware ">" scan; if
|
|
310
|
+
# the bare-DOCTYPE end was misidentified, the preprocessor
|
|
311
|
+
# would rewrite "&" inside the remaining quoted ID region
|
|
312
|
+
# to DEFAULT_AMP_MARKER and leak it into the serialized doctype.
|
|
313
|
+
xml = %(<!DOCTYPE root SYSTEM "a>b&c"><root x="&#38;"/>)
|
|
314
|
+
expect(ctx.parse(xml).root["x"]).to eq("&")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "handles DOCTYPE with ']>' inside a quoted entity value" do
|
|
318
|
+
# Quote-aware DOCTYPE terminator must skip past a literal "]>"
|
|
319
|
+
# appearing inside a quoted ExternalID / ENTITY value so the
|
|
320
|
+
# internal subset terminator is matched correctly. For the Oga
|
|
321
|
+
# adapter this also exercises the source-level DOCTYPE skipper;
|
|
322
|
+
# native-DOCTYPE parsers must reach the same result.
|
|
323
|
+
xml = '<!DOCTYPE root [ <!ENTITY foo "]>"> ]><root x="&#38;"/>'
|
|
324
|
+
expect(ctx.parse(xml).root["x"]).to eq("&")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
SaxEntityParityFixtures::PI_ENTITY_CASES.each do |entity_label, payload|
|
|
328
|
+
it "preserves literal #{entity_label} inside processing instruction" do
|
|
329
|
+
doc = ctx.parse("<doc><?target #{payload}?></doc>")
|
|
330
|
+
pi = doc.root.children.find { |c| c.is_a?(Moxml::ProcessingInstruction) }
|
|
331
|
+
expect(pi.content.strip).to eq(payload)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Cross-adapter equivalence: feed the same XML into every adapter and
|
|
338
|
+
# assert all of them emit the same serialized attribute form. Catches
|
|
339
|
+
# any regression where one adapter starts to deviate from the rest.
|
|
340
|
+
describe "cross-adapter serialization equivalence" do
|
|
341
|
+
def serialize_via(adapter, xml)
|
|
342
|
+
ctx = Moxml.new(adapter)
|
|
343
|
+
ctx.parse(xml).to_xml.sub(/\A<\?[^>]*\?>\s*/, "").strip
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
SaxEntityParityFixtures::ATTRIBUTE_CASES.each do |label, (xml, _expected, expected_serialized)|
|
|
347
|
+
it "every adapter emits the same attribute form: #{label}" do
|
|
348
|
+
results = SaxEntityParityFixtures::ADAPTERS.to_h { |a| [a, serialize_via(a, xml)] }
|
|
349
|
+
results.each do |a, out|
|
|
350
|
+
expect(out).to(
|
|
351
|
+
include(%(x="#{expected_serialized}")),
|
|
352
|
+
"#{a} produced #{out.inspect}",
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::Base64Transform do
|
|
8
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
9
|
+
let(:transform) { described_class.new(context: ctx) }
|
|
10
|
+
|
|
11
|
+
it "decodes base64 octet input" do
|
|
12
|
+
encoded = Base64.strict_encode64("hello world")
|
|
13
|
+
expect(transform.transform(encoded)).to eq("hello world")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "strips whitespace before decoding" do
|
|
17
|
+
encoded = Base64.strict_encode64("hello world").chars.each_slice(4).map(&:join).join("\n")
|
|
18
|
+
expect(transform.transform(encoded)).to eq("hello world")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "raises TransformError on invalid base64" do
|
|
22
|
+
expect do
|
|
23
|
+
transform.transform("!!!not base64!!!")
|
|
24
|
+
end.to raise_error(Moxml::Signature::TransformError)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "base64"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Moxml::Signature::Algorithms::DigestBase do
|
|
9
|
+
describe "SHA-256" do
|
|
10
|
+
let(:digest) { Moxml::Signature::Algorithms::SHA256.new }
|
|
11
|
+
|
|
12
|
+
it "computes raw bytes matching OpenSSL" do
|
|
13
|
+
data = "hello"
|
|
14
|
+
expect(digest.digest(data)).to eq(OpenSSL::Digest::SHA256.digest(data))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "computes base64-encoded digest matching OpenSSL" do
|
|
18
|
+
data = "hello"
|
|
19
|
+
expected = Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(data))
|
|
20
|
+
expect(digest.digest_base64(data)).to eq(expected)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "matches the FIPS 180-3 test vector for empty string" do
|
|
24
|
+
# SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
|
25
|
+
expected_hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
26
|
+
expect(digest.digest("").unpack1("H*")).to eq(expected_hex)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "matches the FIPS 180-3 test vector for 'abc'" do
|
|
30
|
+
expected_hex = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
31
|
+
expect(digest.digest("abc").unpack1("H*")).to eq(expected_hex)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "SHA-1" do
|
|
36
|
+
it "matches the FIPS test vector for 'abc'" do
|
|
37
|
+
digest = Moxml::Signature::Algorithms::SHA1.new
|
|
38
|
+
expected_hex = "a9993e364706816aba3e25717850c26c9cd0d89d"
|
|
39
|
+
expect(digest.digest("abc").unpack1("H*")).to eq(expected_hex)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "is registered under its W3C URI" do
|
|
43
|
+
klass = Moxml::Signature::Algorithms.lookup(
|
|
44
|
+
:digest, "http://www.w3.org/2000/09/xmldsig#sha1"
|
|
45
|
+
)
|
|
46
|
+
expect(klass).to eq(Moxml::Signature::Algorithms::SHA1)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "all five SHA digests" do
|
|
51
|
+
expected_uris = {
|
|
52
|
+
"http://www.w3.org/2000/09/xmldsig#sha1" =>
|
|
53
|
+
[Moxml::Signature::Algorithms::SHA1, 20],
|
|
54
|
+
"http://www.w3.org/2001/04/xmldsig-more#sha224" =>
|
|
55
|
+
[Moxml::Signature::Algorithms::SHA224, 28],
|
|
56
|
+
"http://www.w3.org/2001/04/xmlenc#sha256" =>
|
|
57
|
+
[Moxml::Signature::Algorithms::SHA256, 32],
|
|
58
|
+
"http://www.w3.org/2001/04/xmldsig-more#sha384" =>
|
|
59
|
+
[Moxml::Signature::Algorithms::SHA384, 48],
|
|
60
|
+
"http://www.w3.org/2001/04/xmlenc#sha512" =>
|
|
61
|
+
[Moxml::Signature::Algorithms::SHA512, 64],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expected_uris.each do |uri, (klass, byte_length)|
|
|
65
|
+
it "#{uri} resolves to #{klass.name.split('::').last} with #{byte_length}-byte output" do
|
|
66
|
+
instance = klass.new
|
|
67
|
+
result = instance.digest("test")
|
|
68
|
+
expect(result.bytesize).to eq(byte_length)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::DsaSha do
|
|
8
|
+
let(:key) { OpenSSL::PKey::DSA.generate(2048) }
|
|
9
|
+
let(:data) { "the quick brown fox jumps over the lazy dog" }
|
|
10
|
+
|
|
11
|
+
it "round-trips DSA-SHA1" do
|
|
12
|
+
algo = described_class.new(
|
|
13
|
+
identifier_uri: "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
|
|
14
|
+
)
|
|
15
|
+
sig = algo.sign(data, key)
|
|
16
|
+
# DSA q for 2048-bit DSA is 256 bits → 32-byte halves → 64-byte raw sig.
|
|
17
|
+
expect(sig.bytesize).to eq(64)
|
|
18
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "round-trips DSA-SHA256" do
|
|
22
|
+
algo = described_class.new(
|
|
23
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
24
|
+
)
|
|
25
|
+
sig = algo.sign(data, key)
|
|
26
|
+
expect(sig.bytesize).to eq(64)
|
|
27
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "rejects tampered payload" do
|
|
31
|
+
algo = described_class.new(
|
|
32
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
33
|
+
)
|
|
34
|
+
sig = algo.sign(data, key)
|
|
35
|
+
expect(algo.verify("#{data}!", key, sig)).to be false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "rejects non-DSA keys" do
|
|
39
|
+
algo = described_class.new(
|
|
40
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
41
|
+
)
|
|
42
|
+
expect do
|
|
43
|
+
algo.sign(data, OpenSSL::PKey::RSA.generate(2048))
|
|
44
|
+
end.to raise_error(Moxml::Signature::SignatureKeyError)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::EcdsaSha do
|
|
8
|
+
let(:key) { OpenSSL::PKey::EC.generate("prime256v1") }
|
|
9
|
+
let(:data) { "the quick brown fox jumps over the lazy dog" }
|
|
10
|
+
|
|
11
|
+
%w[
|
|
12
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
|
|
13
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224
|
|
14
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
|
|
15
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
|
|
16
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
|
17
|
+
].each do |uri|
|
|
18
|
+
it "#{uri} round-trips sign/verify on P-256" do
|
|
19
|
+
algo = described_class.new(identifier_uri: uri)
|
|
20
|
+
sig = algo.sign(data, key)
|
|
21
|
+
# P-256: 32-byte r ‖ 32-byte s = 64 bytes
|
|
22
|
+
expect(sig.bytesize).to eq(64)
|
|
23
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "rejects tampered payload" do
|
|
28
|
+
algo = described_class.new(
|
|
29
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
30
|
+
)
|
|
31
|
+
sig = algo.sign(data, key)
|
|
32
|
+
expect(algo.verify("#{data}!", key, sig)).to be false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "rejects verification with a different key" do
|
|
36
|
+
other = OpenSSL::PKey::EC.generate("prime256v1")
|
|
37
|
+
algo = described_class.new(
|
|
38
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
39
|
+
)
|
|
40
|
+
sig = algo.sign(data, key)
|
|
41
|
+
expect(algo.verify(data, other, sig)).to be false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "supports P-384 with 48-byte coordinates" do
|
|
45
|
+
p384 = OpenSSL::PKey::EC.generate("secp384r1")
|
|
46
|
+
algo = described_class.new(
|
|
47
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
|
|
48
|
+
)
|
|
49
|
+
sig = algo.sign(data, p384)
|
|
50
|
+
expect(sig.bytesize).to eq(96)
|
|
51
|
+
expect(algo.verify(data, p384, sig)).to be true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "supports P-521 with 66-byte coordinates" do
|
|
55
|
+
p521 = OpenSSL::PKey::EC.generate("secp521r1")
|
|
56
|
+
algo = described_class.new(
|
|
57
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
|
|
58
|
+
)
|
|
59
|
+
sig = algo.sign(data, p521)
|
|
60
|
+
expect(sig.bytesize).to eq(132)
|
|
61
|
+
expect(algo.verify(data, p521, sig)).to be true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "rejects non-EC keys" do
|
|
65
|
+
algo = described_class.new(
|
|
66
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
67
|
+
)
|
|
68
|
+
expect do
|
|
69
|
+
algo.sign(data, OpenSSL::PKey::RSA.generate(2048))
|
|
70
|
+
end.to raise_error(Moxml::Signature::SignatureKeyError)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Signature::Algorithms::EnvelopedSignatureTransform do
|
|
7
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
8
|
+
|
|
9
|
+
it "is a no-op when signature_element is nil (signing case)" do
|
|
10
|
+
doc = ctx.parse("<root><child/></root>")
|
|
11
|
+
root = doc.root
|
|
12
|
+
transform = described_class.new(context: ctx, signature_element: nil)
|
|
13
|
+
expect(transform.transform(root)).to equal(root)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "removes the containing signature from a copy (verification case)" do
|
|
17
|
+
xml = <<~XML.strip
|
|
18
|
+
<root>
|
|
19
|
+
<payload>important</payload>
|
|
20
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
21
|
+
<ds:SignedInfo/>
|
|
22
|
+
</ds:Signature>
|
|
23
|
+
</root>
|
|
24
|
+
XML
|
|
25
|
+
doc = ctx.parse(xml)
|
|
26
|
+
sig_elem = doc.at_xpath("//ds:Signature",
|
|
27
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#")
|
|
28
|
+
|
|
29
|
+
transform = described_class.new(context: ctx, signature_element: sig_elem)
|
|
30
|
+
result = transform.transform(doc.root)
|
|
31
|
+
|
|
32
|
+
expect(doc.at_xpath("//ds:Signature",
|
|
33
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#")).not_to be_nil
|
|
34
|
+
|
|
35
|
+
c14n = Moxml::C14n::Exclusive.new
|
|
36
|
+
canonical = c14n.canonicalize(result)
|
|
37
|
+
expect(canonical).not_to include("Signature")
|
|
38
|
+
expect(canonical).to include("important")
|
|
39
|
+
end
|
|
40
|
+
end
|