moxml 0.1.24 → 0.1.26
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/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +188 -0
- data/Rakefile +116 -4
- 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 +4 -0
- data/lib/moxml/adapter.rb +5 -3
- 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 +1 -1
- 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 +29 -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.rb +2 -0
- data/moxml.gemspec +1 -1
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- 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/moxml/adapter/platform_spec.rb +64 -46
- 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/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -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/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- metadata +115 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# xml:lang and xml:space are "simple inheritable" attributes per W3C C14N
|
|
7
|
+
# 1.1 §2.4. When an element is in the node-set but an ancestor is not,
|
|
8
|
+
# the canonical form inherits these attributes from the nearest ancestor
|
|
9
|
+
# in which they were declared.
|
|
10
|
+
#
|
|
11
|
+
# These specs exercise the inclusive behavior at full-document
|
|
12
|
+
# canonicalization (where everything is in the set, so inheritance
|
|
13
|
+
# doesn't trigger). For subset scenarios, see TODO.c14n/07.
|
|
14
|
+
RSpec.describe "C14N xml:* inheritable attributes" do
|
|
15
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
16
|
+
|
|
17
|
+
it "renders xml:lang attribute" do
|
|
18
|
+
doc = ctx.parse(%(<root xml:lang="en">hello</root>))
|
|
19
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
20
|
+
.to eq(%(<root xml:lang="en">hello</root>))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "renders xml:space attribute" do
|
|
24
|
+
doc = ctx.parse(%(<root xml:space="preserve">data</root>))
|
|
25
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
26
|
+
.to eq(%(<root xml:space="preserve">data</root>))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "renders xml:lang on descendant that declares its own" do
|
|
30
|
+
doc = ctx.parse(%(<a xml:lang="en"><b xml:lang="fr"/></a>))
|
|
31
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
32
|
+
expect(result).to include('<b xml:lang="fr">')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "renders xml:id attribute" do
|
|
36
|
+
doc = ctx.parse(%(<root xml:id="foo"/>))
|
|
37
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
38
|
+
.to eq(%(<root xml:id="foo"></root>))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "renders xml:base attribute" do
|
|
42
|
+
doc = ctx.parse(%(<root xml:base="http://example.com/"/>))
|
|
43
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
44
|
+
.to eq(%(<root xml:base="http://example.com/"></root>))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "sorts xml:* attributes by namespace URI then local name" do
|
|
48
|
+
doc = ctx.parse(%(<root xml:space="default" xml:lang="en" xml:id="x"/>))
|
|
49
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
50
|
+
# All in XML namespace; sort by local name: id < lang < space
|
|
51
|
+
expect(result.index("xml:id=")).to be < result.index("xml:lang=")
|
|
52
|
+
expect(result.index("xml:lang=")).to be < result.index("xml:space=")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/adapter/shared_examples/adapter_contract"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Adapter::Oga, if: RUBY_ENGINE == "opal" do
|
|
7
|
+
around do |example|
|
|
8
|
+
Moxml.with_config(:oga, true, "UTF-8") do
|
|
9
|
+
example.run
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it_behaves_like "xml adapter"
|
|
14
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Exercises feature areas under Opal that are not covered by the shared
|
|
6
|
+
# adapter contract (opal_oga_adapter_spec.rb) or the basic smoke spec
|
|
7
|
+
# (opal_oga_smoke_spec.rb). Targets the default Opal adapter (:oga).
|
|
8
|
+
# Uses an explicit :oga context so leaked global config from other specs
|
|
9
|
+
# cannot flip the adapter under test.
|
|
10
|
+
RSpec.describe "Moxml Opal oga feature coverage", if: RUBY_ENGINE == "opal" do
|
|
11
|
+
let(:context) { Moxml.new(:oga) }
|
|
12
|
+
|
|
13
|
+
describe "builder DSL" do
|
|
14
|
+
it "builds a document with the block DSL" do
|
|
15
|
+
doc = Moxml::Builder.new(context).build do
|
|
16
|
+
root do
|
|
17
|
+
child "text"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
expect(doc.root.name).to eq("root")
|
|
22
|
+
expect(doc.root.children.first.name).to eq("child")
|
|
23
|
+
expect(doc.root.children.first.text).to eq("text")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "creates nested elements via blocks" do
|
|
27
|
+
doc = Moxml::Builder.new(context).build do
|
|
28
|
+
library do
|
|
29
|
+
book(id: "1") { title "A" }
|
|
30
|
+
book(id: "2") { title "B" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
books = doc.root.children.grep(Moxml::Element)
|
|
35
|
+
expect(books.length).to eq(2)
|
|
36
|
+
expect(books.map { |b| b["id"] }).to eq(%w[1 2])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "supports method_missing DSL with attributes and text" do
|
|
40
|
+
doc = Moxml::Builder.new(context).build do
|
|
41
|
+
person(name: "Alice", age: "30") { email "alice@example.com" }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
person = doc.root
|
|
45
|
+
expect(person.name).to eq("person")
|
|
46
|
+
expect(person["name"]).to eq("Alice")
|
|
47
|
+
expect(person["age"]).to eq("30")
|
|
48
|
+
expect(person.children.first.name).to eq("email")
|
|
49
|
+
expect(person.children.first.text).to eq("alice@example.com")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "strips trailing underscore for reserved-name tags" do
|
|
53
|
+
doc = Moxml::Builder.new(context).build do
|
|
54
|
+
class_ { name "Foo" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
expect(doc.root.name).to eq("class")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "attaches namespace declarations" do
|
|
61
|
+
builder = Moxml::Builder.new(context)
|
|
62
|
+
doc = builder.build do
|
|
63
|
+
root("xmlns:dc": "http://purl.org/dc/elements/1.1/") do
|
|
64
|
+
builder.element("dc:title") { builder.text "Hello" }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
title = doc.root.children.find { |c| c.is_a?(Moxml::Element) }
|
|
69
|
+
expect(title.name).to eq("dc:title")
|
|
70
|
+
expect(doc.to_xml).to include("xmlns:dc")
|
|
71
|
+
expect(doc.to_xml).to include("dc:title")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "XML declaration preservation" do
|
|
76
|
+
it "does not add a declaration when the input has none" do
|
|
77
|
+
doc = context.parse("<root><child/></root>")
|
|
78
|
+
output = doc.to_xml
|
|
79
|
+
|
|
80
|
+
expect(output).not_to include("<?xml")
|
|
81
|
+
expect(doc.has_xml_declaration).to be false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "preserves the declaration when the input has one" do
|
|
85
|
+
xml = '<?xml version="1.0" encoding="UTF-8"?><root/>'
|
|
86
|
+
doc = context.parse(xml)
|
|
87
|
+
|
|
88
|
+
expect(doc.has_xml_declaration).to be true
|
|
89
|
+
expect(doc.to_xml).to include("<?xml")
|
|
90
|
+
expect(doc.to_xml).to include('version="1.0"')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "forces the declaration on with declaration: true" do
|
|
94
|
+
doc = context.parse("<root/>")
|
|
95
|
+
expect(doc.to_xml(declaration: true)).to include("<?xml")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "forces the declaration off with declaration: false" do
|
|
99
|
+
doc = context.parse('<?xml version="1.0"?><root/>')
|
|
100
|
+
expect(doc.to_xml(declaration: false)).not_to include("<?xml")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "preserves standalone attribute" do
|
|
104
|
+
xml = '<?xml version="1.0" standalone="yes"?><root/>'
|
|
105
|
+
doc = context.parse(xml)
|
|
106
|
+
expect(doc.to_xml).to include("standalone")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "doctype handling" do
|
|
111
|
+
it "parses a SIMPLE doctype" do
|
|
112
|
+
doc = context.parse("<!DOCTYPE root><root/>")
|
|
113
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
114
|
+
expect(doctype).not_to be_nil
|
|
115
|
+
expect(doctype.name).to eq("root")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "parses a PUBLIC doctype with external and system ids" do
|
|
119
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html/>'
|
|
120
|
+
doc = context.parse(xml)
|
|
121
|
+
doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
|
|
122
|
+
|
|
123
|
+
expect(doctype).not_to be_nil
|
|
124
|
+
expect(doctype.name).to eq("html")
|
|
125
|
+
expect(doctype.external_id).to eq("-//W3C//DTD HTML 4.01//EN")
|
|
126
|
+
expect(doctype.system_id).to eq("http://www.w3.org/TR/html4/strict.dtd")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "round-trips the doctype through serialization" do
|
|
130
|
+
xml = "<!DOCTYPE root><root/>"
|
|
131
|
+
doc = context.parse(xml)
|
|
132
|
+
expect(doc.to_xml).to include("DOCTYPE")
|
|
133
|
+
expect(doc.to_xml).to include("root")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "round-trip stability" do
|
|
138
|
+
it "round-trips a document with mixed content" do
|
|
139
|
+
xml = <<~XML.strip
|
|
140
|
+
<root attr="value">
|
|
141
|
+
<child>text</child>
|
|
142
|
+
<!-- comment -->
|
|
143
|
+
<nested><deep>data</deep></nested>
|
|
144
|
+
</root>
|
|
145
|
+
XML
|
|
146
|
+
|
|
147
|
+
doc1 = context.parse(xml)
|
|
148
|
+
serialized1 = doc1.to_xml
|
|
149
|
+
doc2 = context.parse(serialized1)
|
|
150
|
+
serialized2 = doc2.to_xml
|
|
151
|
+
|
|
152
|
+
expect(serialized2).to eq(serialized1)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "round-trips a document with namespaces" do
|
|
156
|
+
xml = '<root xmlns:ns="http://example.com/ns"><ns:child>content</ns:child></root>'
|
|
157
|
+
doc1 = context.parse(xml)
|
|
158
|
+
serialized1 = doc1.to_xml
|
|
159
|
+
|
|
160
|
+
doc2 = context.parse(serialized1)
|
|
161
|
+
child = doc2.root.children.find { |c| c.is_a?(Moxml::Element) }
|
|
162
|
+
expect(child.name).to eq("child")
|
|
163
|
+
expect(child.namespace_prefix).to eq("ns")
|
|
164
|
+
expect(doc2.to_xml).to include("xmlns:ns")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "round-trips a document with CDATA" do
|
|
168
|
+
xml = "<root><![CDATA[<unparsed>content</unparsed>]]></root>"
|
|
169
|
+
doc1 = context.parse(xml)
|
|
170
|
+
serialized1 = doc1.to_xml
|
|
171
|
+
|
|
172
|
+
expect(serialized1).to include("<![CDATA[")
|
|
173
|
+
doc2 = context.parse(serialized1)
|
|
174
|
+
expect(doc2.to_xml).to include("<![CDATA[")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe "XPath under Opal" do
|
|
179
|
+
it "evaluates element-only XPath" do
|
|
180
|
+
doc = context.parse("<root><a><b>1</b></a><a><b>2</b></a></root>")
|
|
181
|
+
results = doc.xpath("//a/b")
|
|
182
|
+
expect(results.length).to eq(2)
|
|
183
|
+
expect(results.map(&:text)).to eq(%w[1 2])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "supports attribute predicates" do
|
|
187
|
+
doc = context.parse('<root><item id="x">1</item><item id="y">2</item></root>')
|
|
188
|
+
found = doc.xpath("//item").find { |i| i["id"] == "y" }
|
|
189
|
+
expect(found.text).to eq("2")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "supports at_xpath" do
|
|
193
|
+
doc = context.parse("<root><a>1</a><a>2</a></root>")
|
|
194
|
+
first = doc.at_xpath("//a")
|
|
195
|
+
expect(first.text).to eq("1")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe "comment and processing instruction handling" do
|
|
200
|
+
it "preserves comments through parse/serialize" do
|
|
201
|
+
xml = "<root><!-- my comment --></root>"
|
|
202
|
+
doc = context.parse(xml)
|
|
203
|
+
expect(doc.to_xml).to include("my comment")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "preserves processing instructions through parse/serialize" do
|
|
207
|
+
xml = "<?pi-target pi-data?><root/>"
|
|
208
|
+
doc = context.parse(xml)
|
|
209
|
+
expect(doc.to_xml).to include("pi-target")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Verifies that the Opal default adapter (:oga, set in Moxml::Config)
|
|
6
|
+
# loads correctly via the Opal-compatible oga fork. The fork's
|
|
7
|
+
# lib/oga.rb dispatches `require 'liboga'` to `require 'oga/native/lexer'`
|
|
8
|
+
# under Opal, which resolves to the pure-Ruby lexer vendored at
|
|
9
|
+
# vendor/opal-oga/ext/pureruby/.
|
|
10
|
+
RSpec.describe "Moxml Opal oga default", if: RUBY_ENGINE == "opal" do
|
|
11
|
+
let(:context) { Moxml.new }
|
|
12
|
+
|
|
13
|
+
it "defaults to :oga under Opal" do
|
|
14
|
+
expect(Moxml::Config::OPAL_DEFAULT_ADAPTER).to eq(:oga)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "parses XML through the oga adapter" do
|
|
18
|
+
doc = context.parse("<root><child>text</child></root>")
|
|
19
|
+
expect(doc.root.name).to eq("root")
|
|
20
|
+
expect(doc.root.children.first.name).to eq("child")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "decodes entities single-pass" do
|
|
24
|
+
doc = context.parse("<root>&#38;</root>")
|
|
25
|
+
expect(doc.root.text).to eq("&")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "serializes back to XML" do
|
|
29
|
+
xml = '<person name="Alice"><age>30</age></person>'
|
|
30
|
+
doc = context.parse(xml)
|
|
31
|
+
serialized = doc.to_xml
|
|
32
|
+
expect(serialized).to include("person")
|
|
33
|
+
expect(serialized).to include("Alice")
|
|
34
|
+
end
|
|
35
|
+
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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::HmacSha do
|
|
8
|
+
let(:secret) { "super-secret-shared-key" }
|
|
9
|
+
let(:data) { "the quick brown fox" }
|
|
10
|
+
|
|
11
|
+
describe "HMAC-SHA256" do
|
|
12
|
+
let(:uri) { "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" }
|
|
13
|
+
let(:algo) { described_class.new(identifier_uri: uri) }
|
|
14
|
+
|
|
15
|
+
it "produces a verifiable MAC" do
|
|
16
|
+
mac = algo.sign(data, secret)
|
|
17
|
+
expect(algo.verify(data, secret, mac)).to be true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "rejects a tampered payload" do
|
|
21
|
+
mac = algo.sign(data, secret)
|
|
22
|
+
expect(algo.verify("#{data}!", secret, mac)).to be false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "matches OpenSSL HMAC for the same inputs" do
|
|
26
|
+
mac = algo.sign(data, secret)
|
|
27
|
+
expected = OpenSSL::HMAC.digest("SHA256", secret, data)
|
|
28
|
+
expect(mac).to eq(expected)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "HMAC truncation" do
|
|
33
|
+
let(:uri) { "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" }
|
|
34
|
+
|
|
35
|
+
it "truncates to the specified bit length" do
|
|
36
|
+
algo = described_class.new(
|
|
37
|
+
identifier_uri: uri,
|
|
38
|
+
parameters: { hmac_output_length: 128 },
|
|
39
|
+
)
|
|
40
|
+
mac = algo.sign(data, secret)
|
|
41
|
+
expect(mac.bytesize).to eq(16)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "rejects truncation below hash_bits / 2" do
|
|
45
|
+
expect do
|
|
46
|
+
described_class.new(
|
|
47
|
+
identifier_uri: uri,
|
|
48
|
+
parameters: { hmac_output_length: 64 },
|
|
49
|
+
)
|
|
50
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "rejects truncation below 80 bits even for SHA-1" do
|
|
54
|
+
expect do
|
|
55
|
+
described_class.new(
|
|
56
|
+
identifier_uri: "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
|
|
57
|
+
parameters: { hmac_output_length: 72 },
|
|
58
|
+
)
|
|
59
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "rejects truncation that is not a multiple of 8" do
|
|
63
|
+
expect do
|
|
64
|
+
described_class.new(
|
|
65
|
+
identifier_uri: uri,
|
|
66
|
+
parameters: { hmac_output_length: 130 },
|
|
67
|
+
)
|
|
68
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|