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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +1 -1
  3. data/.github/workflows/opal.yml +18 -1
  4. data/.github/workflows/rake.yml +32 -3
  5. data/.github/workflows/release.yml +9 -0
  6. data/.github/workflows/round-trip.yml +72 -9
  7. data/.gitmodules +6 -0
  8. data/.rubocop_todo.yml +2 -0
  9. data/Gemfile +9 -1
  10. data/README.adoc +188 -0
  11. data/Rakefile +116 -4
  12. data/docs/signature/algorithms.md +108 -0
  13. data/docs/signature/architecture.md +148 -0
  14. data/docs/signature/c14n.md +110 -0
  15. data/docs/signature/examples.md +109 -0
  16. data/docs/signature/flows.md +132 -0
  17. data/docs/signature/quick-reference.md +115 -0
  18. data/docs/signature/security.md +143 -0
  19. data/examples/signature/auto_key_extraction.rb +24 -0
  20. data/examples/signature/enveloped_rsa.rb +45 -0
  21. data/examples/signature/hmac.rb +33 -0
  22. data/lib/compat/opal/moxml_boot.rb +4 -0
  23. data/lib/moxml/adapter.rb +5 -3
  24. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  25. data/lib/moxml/c14n/character_encoder.rb +29 -0
  26. data/lib/moxml/c14n/data_model.rb +145 -0
  27. data/lib/moxml/c14n/exclusive.rb +188 -0
  28. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  29. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  30. data/lib/moxml/c14n/namespace_context.rb +68 -0
  31. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  32. data/lib/moxml/c14n/node.rb +42 -0
  33. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  34. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  35. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  36. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  37. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  38. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  39. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  40. data/lib/moxml/c14n/nodes.rb +17 -0
  41. data/lib/moxml/c14n/processor.rb +118 -0
  42. data/lib/moxml/c14n/writer.rb +121 -0
  43. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  44. data/lib/moxml/c14n.rb +96 -0
  45. data/lib/moxml/config.rb +1 -1
  46. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  47. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  48. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  49. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  50. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  51. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  52. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  53. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  54. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  55. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  56. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  57. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  58. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  59. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  60. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  61. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  62. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  63. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  64. data/lib/moxml/signature/algorithms.rb +113 -0
  65. data/lib/moxml/signature/errors.rb +99 -0
  66. data/lib/moxml/signature/key_extractor.rb +172 -0
  67. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  68. data/lib/moxml/signature/model/digest_method.rb +15 -0
  69. data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
  70. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  71. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  72. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  73. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  74. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  75. data/lib/moxml/signature/model/key_info.rb +21 -0
  76. data/lib/moxml/signature/model/key_value.rb +20 -0
  77. data/lib/moxml/signature/model/object_element.rb +18 -0
  78. data/lib/moxml/signature/model/reference.rb +22 -0
  79. data/lib/moxml/signature/model/signature.rb +21 -0
  80. data/lib/moxml/signature/model/signature_value.rb +16 -0
  81. data/lib/moxml/signature/model/signed_info.rb +20 -0
  82. data/lib/moxml/signature/model/transform.rb +16 -0
  83. data/lib/moxml/signature/model/transforms.rb +36 -0
  84. data/lib/moxml/signature/model.rb +35 -0
  85. data/lib/moxml/signature/parser.rb +270 -0
  86. data/lib/moxml/signature/reference_resolver.rb +79 -0
  87. data/lib/moxml/signature/reference_result.rb +22 -0
  88. data/lib/moxml/signature/serializer.rb +163 -0
  89. data/lib/moxml/signature/signer.rb +73 -0
  90. data/lib/moxml/signature/single_verification_result.rb +31 -0
  91. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  92. data/lib/moxml/signature/verification_result.rb +27 -0
  93. data/lib/moxml/signature/verifier.rb +129 -0
  94. data/lib/moxml/signature.rb +94 -0
  95. data/lib/moxml/version.rb +1 -1
  96. data/lib/moxml.rb +2 -0
  97. data/moxml.gemspec +1 -1
  98. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  99. data/reference-docs/w3c-xmldsig-core.md +400 -0
  100. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  101. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  102. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  103. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  104. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  105. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  106. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  107. data/spec/moxml/adapter/platform_spec.rb +64 -46
  108. data/spec/moxml/c14n/api_spec.rb +117 -0
  109. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  110. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  111. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  112. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  113. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  114. data/spec/moxml/opal_oga_adapter_spec.rb +14 -0
  115. data/spec/moxml/opal_oga_features_spec.rb +212 -0
  116. data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
  117. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  118. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  119. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  120. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  121. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  122. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  123. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  124. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  125. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  126. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  127. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  128. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  129. data/spec/moxml/signature/model_spec.rb +54 -0
  130. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  131. data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
  132. data/spec/spec_helper.rb +1 -1
  133. 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>&amp;#38;</root>")
25
+ expect(doc.root.text).to eq("&#38;")
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