moxml 0.1.25 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/.github/workflows/windows.yml +62 -0
  4. data/Gemfile +3 -1
  5. data/README.adoc +133 -6
  6. data/docs/signature/algorithms.md +108 -0
  7. data/docs/signature/architecture.md +148 -0
  8. data/docs/signature/c14n.md +110 -0
  9. data/docs/signature/examples.md +109 -0
  10. data/docs/signature/flows.md +132 -0
  11. data/docs/signature/quick-reference.md +115 -0
  12. data/docs/signature/security.md +143 -0
  13. data/examples/signature/auto_key_extraction.rb +24 -0
  14. data/examples/signature/enveloped_rsa.rb +45 -0
  15. data/examples/signature/hmac.rb +33 -0
  16. data/lib/compat/opal/moxml_boot.rb +5 -0
  17. data/lib/moxml/adapter/base.rb +53 -49
  18. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  19. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  20. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  21. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  22. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  23. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  24. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  25. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  26. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  27. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  28. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  29. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  30. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  31. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  32. data/lib/moxml/adapter/customized_oga.rb +2 -0
  33. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  34. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  35. data/lib/moxml/adapter/headed_ox.rb +9 -128
  36. data/lib/moxml/adapter/leptris.rb +965 -0
  37. data/lib/moxml/adapter/libxml.rb +131 -99
  38. data/lib/moxml/adapter/nokogiri.rb +29 -8
  39. data/lib/moxml/adapter/oga.rb +91 -54
  40. data/lib/moxml/adapter/ox.rb +93 -137
  41. data/lib/moxml/adapter/rexml.rb +61 -50
  42. data/lib/moxml/adapter.rb +2 -1
  43. data/lib/moxml/attribute.rb +8 -4
  44. data/lib/moxml/attribute_resolver.rb +189 -0
  45. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  46. data/lib/moxml/c14n/character_encoder.rb +29 -0
  47. data/lib/moxml/c14n/data_model.rb +145 -0
  48. data/lib/moxml/c14n/exclusive.rb +188 -0
  49. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  50. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  51. data/lib/moxml/c14n/namespace_context.rb +68 -0
  52. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  53. data/lib/moxml/c14n/node.rb +42 -0
  54. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  55. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  56. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  57. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  58. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  59. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  60. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  61. data/lib/moxml/c14n/nodes.rb +17 -0
  62. data/lib/moxml/c14n/processor.rb +118 -0
  63. data/lib/moxml/c14n/writer.rb +121 -0
  64. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  65. data/lib/moxml/c14n.rb +96 -0
  66. data/lib/moxml/config.rb +29 -3
  67. data/lib/moxml/context.rb +12 -0
  68. data/lib/moxml/element.rb +29 -12
  69. data/lib/moxml/entity/reference.rb +28 -0
  70. data/lib/moxml/entity.rb +125 -0
  71. data/lib/moxml/node.rb +90 -1
  72. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  73. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  74. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  75. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  76. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  77. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  78. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  79. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  80. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  81. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  82. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  83. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  84. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  85. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  86. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  87. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  88. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  89. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  90. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  91. data/lib/moxml/signature/algorithms.rb +113 -0
  92. data/lib/moxml/signature/errors.rb +99 -0
  93. data/lib/moxml/signature/key_extractor.rb +172 -0
  94. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  95. data/lib/moxml/signature/model/digest_method.rb +15 -0
  96. data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
  97. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  98. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  99. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  100. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  101. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  102. data/lib/moxml/signature/model/key_info.rb +21 -0
  103. data/lib/moxml/signature/model/key_value.rb +20 -0
  104. data/lib/moxml/signature/model/object_element.rb +18 -0
  105. data/lib/moxml/signature/model/reference.rb +22 -0
  106. data/lib/moxml/signature/model/signature.rb +21 -0
  107. data/lib/moxml/signature/model/signature_value.rb +16 -0
  108. data/lib/moxml/signature/model/signed_info.rb +20 -0
  109. data/lib/moxml/signature/model/transform.rb +16 -0
  110. data/lib/moxml/signature/model/transforms.rb +36 -0
  111. data/lib/moxml/signature/model.rb +35 -0
  112. data/lib/moxml/signature/parser.rb +270 -0
  113. data/lib/moxml/signature/reference_resolver.rb +79 -0
  114. data/lib/moxml/signature/reference_result.rb +22 -0
  115. data/lib/moxml/signature/serializer.rb +163 -0
  116. data/lib/moxml/signature/signer.rb +73 -0
  117. data/lib/moxml/signature/single_verification_result.rb +31 -0
  118. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  119. data/lib/moxml/signature/verification_result.rb +27 -0
  120. data/lib/moxml/signature/verifier.rb +129 -0
  121. data/lib/moxml/signature.rb +94 -0
  122. data/lib/moxml/version.rb +1 -1
  123. data/lib/moxml/xml_emitter.rb +71 -0
  124. data/lib/moxml/xpath/compiler.rb +60 -7
  125. data/lib/moxml/xpath/parser.rb +22 -15
  126. data/lib/moxml.rb +5 -0
  127. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  128. data/reference-docs/w3c-xmldsig-core.md +400 -0
  129. data/spec/examples/readme_examples_spec.rb +0 -4
  130. data/spec/examples/xpath_examples_spec.rb +0 -11
  131. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  132. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  133. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  134. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  135. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  136. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  137. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  138. data/spec/integration/all_adapters_spec.rb +17 -0
  139. data/spec/integration/sax_parity_spec.rb +58 -0
  140. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  141. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  142. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  143. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  144. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  145. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  146. data/spec/moxml/adapter/ox_spec.rb +20 -6
  147. data/spec/moxml/adapter/platform_spec.rb +15 -2
  148. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  149. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  150. data/spec/moxml/c14n/api_spec.rb +117 -0
  151. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  152. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  153. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  154. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  155. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  156. data/spec/moxml/doctype_spec.rb +1 -1
  157. data/spec/moxml/entity_spec.rb +76 -0
  158. data/spec/moxml/node_spec.rb +104 -0
  159. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  160. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  161. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  162. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  163. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  164. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  165. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  166. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  167. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  168. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  169. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  170. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  171. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  172. data/spec/moxml/signature/model_spec.rb +54 -0
  173. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  174. data/spec/moxml/xml_emitter_spec.rb +64 -0
  175. data/spec/moxml/xpath/axes_spec.rb +72 -0
  176. data/spec/moxml/xpath/parser_spec.rb +6 -0
  177. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  178. data/spec/performance/benchmark_spec.rb +13 -6
  179. data/spec/performance/thread_safety_spec.rb +0 -4
  180. data/spec/spec_helper.rb +5 -1
  181. metadata +130 -2
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+ require "openssl"
6
+
7
+ RSpec.describe Moxml::Signature::KeyExtractor do
8
+ let(:extractor) { described_class.new }
9
+
10
+ describe "RSAKeyValue" do
11
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
12
+
13
+ it "reconstructs a public RSA key from Modulus and Exponent" do
14
+ require "base64"
15
+ n_b64 = Base64.strict_encode64(rsa_key.n.to_s(2))
16
+ e_b64 = Base64.strict_encode64(rsa_key.e.to_s(2))
17
+ key_value = Moxml::Signature::Model::KeyValue.new(
18
+ rsa_key_value: Moxml::Signature::Model::Key::RSAKeyValue.new(
19
+ modulus: n_b64,
20
+ exponent: e_b64,
21
+ ),
22
+ )
23
+ key_info = Moxml::Signature::Model::KeyInfo.new(key_value: key_value)
24
+ extracted = extractor.extract(key_info)
25
+ expect(extracted).to be_a(OpenSSL::PKey::RSA)
26
+ expect(extracted.n).to eq(rsa_key.n)
27
+ expect(extracted.e).to eq(rsa_key.e)
28
+ end
29
+ end
30
+
31
+ describe "X509Certificate" do
32
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
33
+ let(:cert) do
34
+ # Generate a self-signed cert so we can round-trip the DER.
35
+ cert = OpenSSL::X509::Certificate.new
36
+ cert.version = 2
37
+ cert.serial = 1
38
+ cert.subject = OpenSSL::X509::Name.parse("/CN=test")
39
+ cert.issuer = cert.subject
40
+ cert.public_key = rsa_key
41
+ cert.not_before = Time.now
42
+ cert.not_after = Time.now + 3600
43
+ cert.sign(rsa_key, OpenSSL::Digest.new("SHA256"))
44
+ cert
45
+ end
46
+
47
+ it "extracts the public key from the certificate" do
48
+ require "base64"
49
+ key_info = Moxml::Signature::Model::KeyInfo.new(
50
+ x509_data: Moxml::Signature::Model::Key::X509Data.new(
51
+ # X509Certificate is stored as base64-encoded DER (spec §4.5.4).
52
+ certificates: [Base64.strict_encode64(cert.to_der)],
53
+ ),
54
+ )
55
+ extracted = extractor.extract(key_info)
56
+ expect(extracted).to be_a(OpenSSL::PKey::RSA)
57
+ expect(extracted.n).to eq(rsa_key.n)
58
+ end
59
+ end
60
+
61
+ describe "KeyName" do
62
+ it "looks up the key in the application-supplied map" do
63
+ key = OpenSSL::PKey::RSA.generate(2048)
64
+ extractor_with_map = described_class.new(key_map: { "my-key" => key })
65
+ key_info = Moxml::Signature::Model::KeyInfo.new(key_name: "my-key")
66
+ expect(extractor_with_map.extract(key_info)).to equal(key)
67
+ end
68
+
69
+ it "returns nil for an unknown key name" do
70
+ key_info = Moxml::Signature::Model::KeyInfo.new(key_name: "unknown")
71
+ expect(extractor.extract(key_info)).to be_nil
72
+ end
73
+ end
74
+
75
+ describe "ECKeyValue" do
76
+ it "reconstructs a P-256 public EC key" do
77
+ ec = OpenSSL::PKey::EC.generate("prime256v1")
78
+ # Spec §4.5.2.3: PublicKey contains the uncompressed-point form
79
+ # 0x04 || x || y, base64-encoded.
80
+ point_octets = ec.public_key.to_octet_string(:uncompressed)
81
+ ec_pub_b64 = Base64.strict_encode64(point_octets)
82
+
83
+ key_info = Moxml::Signature::Model::KeyInfo.new(
84
+ key_value: Moxml::Signature::Model::KeyValue.new(
85
+ ec_key_value: Moxml::Signature::Model::Key::ECKeyValue.new(
86
+ named_curve_uri: "urn:oid:1.2.840.10045.3.1.7",
87
+ public_key: ec_pub_b64,
88
+ ),
89
+ ),
90
+ )
91
+ extracted = extractor.extract(key_info)
92
+ expect(extracted).to be_a(OpenSSL::PKey::EC)
93
+ expect(extracted.public_key).to eq(ec.public_key)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+
6
+ RSpec.describe Moxml::Signature::Model do
7
+ describe Moxml::Signature::Model::Signature do
8
+ it "stores its components" do
9
+ si = Moxml::Signature::Model::SignedInfo.new
10
+ sv = Moxml::Signature::Model::SignatureValue.new(value: "abc")
11
+ sig = described_class.new(id: "S1", signed_info: si, signature_value: sv)
12
+ expect(sig.id).to eq("S1")
13
+ expect(sig.signed_info).to equal(si)
14
+ expect(sig.signature_value).to equal(sv)
15
+ expect(sig.objects).to eq([])
16
+ end
17
+ end
18
+
19
+ describe Moxml::Signature::Model::Reference do
20
+ it "accepts uri, transforms, and digest method" do
21
+ t = Moxml::Signature::Model::Transforms.new(
22
+ transforms: [Moxml::Signature::Model::Transform.new(
23
+ algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
24
+ )],
25
+ )
26
+ dm = Moxml::Signature::Model::DigestMethod.new(
27
+ algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
28
+ )
29
+ ref = described_class.new(uri: "", transforms: t, digest_method: dm,
30
+ digest_value: "abc=")
31
+ expect(ref.uri).to eq("")
32
+ expect(ref.transforms.transforms.size).to eq(1)
33
+ expect(ref.digest_method.algorithm).to eq(
34
+ "http://www.w3.org/2001/04/xmlenc#sha256",
35
+ )
36
+ expect(ref.digest_value).to eq("abc=")
37
+ end
38
+ end
39
+
40
+ describe Moxml::Signature::Model::Transforms do
41
+ it "is empty by default" do
42
+ expect(described_class.new).to be_empty
43
+ end
44
+
45
+ it "appends transforms with <<" do
46
+ transforms = described_class.new
47
+ t = Moxml::Signature::Model::Transform.new(
48
+ algorithm: "http://www.w3.org/2000/09/xmldsig#base64",
49
+ )
50
+ transforms << t
51
+ expect(transforms.size).to eq(1)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+ require "openssl"
6
+
7
+ # End-to-end signing + verification, including tamper detection and
8
+ # wrong-key rejection. Uses real model instances and real OpenSSL keys
9
+ # per the project's "no doubles" rule.
10
+ RSpec.describe "Moxml XML Signature end-to-end round trip" do
11
+ let(:ctx) { Moxml.new(:nokogiri) }
12
+ let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
13
+ let(:document_xml) { "<doc><greeting>Hello, World!</greeting></doc>" }
14
+
15
+ let(:common_options) do
16
+ {
17
+ context: ctx,
18
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
19
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
20
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
21
+ reference_uri: "",
22
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
23
+ }
24
+ end
25
+
26
+ def sign_and_attach(xml, key, **opts)
27
+ doc = ctx.parse(xml)
28
+ signature = Moxml::Signature.sign(document: doc, key: key, **common_options.merge(opts))
29
+ serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
30
+ doc.root.add_child(serialized.root)
31
+ doc
32
+ end
33
+
34
+ describe "RSA-SHA256 round trip" do
35
+ it "verifies a freshly-signed document" do
36
+ doc = sign_and_attach(document_xml, private_key)
37
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
38
+ expect(result.valid?).to be true
39
+ expect(result.signature_count).to eq(1)
40
+ end
41
+
42
+ it "verifies with the public key alone" do
43
+ doc = sign_and_attach(document_xml, private_key)
44
+ pub = OpenSSL::PKey::RSA.new(private_key.public_to_pem)
45
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: pub)
46
+ expect(result.valid?).to be true
47
+ end
48
+
49
+ it "detects payload tampering" do
50
+ doc = sign_and_attach(document_xml, private_key)
51
+ greeting = doc.at_xpath("//greeting")
52
+ greeting.text = "Goodbye, World!"
53
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
54
+ expect(result.valid?).to be false
55
+ # SignatureValue still matches (SignedInfo unchanged) but reference
56
+ # digest mismatches.
57
+ expect(result.results.first.signature_valid?).to be true
58
+ expect(result.results.first.references.first.valid?).to be false
59
+ end
60
+
61
+ it "detects SignedInfo tampering" do
62
+ doc = sign_and_attach(document_xml, private_key)
63
+ dv = doc.at_xpath("//ds:DigestValue",
64
+ "ds" => "http://www.w3.org/2000/09/xmldsig#")
65
+ dv.text = Base64.strict_encode64("x" * 32)
66
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
67
+ expect(result.valid?).to be false
68
+ end
69
+
70
+ it "rejects an unrelated verification key" do
71
+ doc = sign_and_attach(document_xml, private_key)
72
+ other = OpenSSL::PKey::RSA.generate(2048)
73
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: other)
74
+ expect(result.valid?).to be false
75
+ expect(result.results.first.signature_valid?).to be false
76
+ end
77
+ end
78
+
79
+ describe "HMAC-SHA256 round trip" do
80
+ let(:hmac_options) do
81
+ common_options.merge(
82
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
83
+ )
84
+ end
85
+
86
+ it "verifies with the shared secret" do
87
+ doc = sign_and_attach(document_xml, "shared-secret", **hmac_options)
88
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: "shared-secret")
89
+ expect(result.valid?).to be true
90
+ end
91
+
92
+ it "rejects the wrong secret" do
93
+ doc = sign_and_attach(document_xml, "shared-secret", **hmac_options)
94
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: "wrong-secret")
95
+ expect(result.valid?).to be false
96
+ end
97
+ end
98
+
99
+ describe "fixture key" do
100
+ let(:pem_path) do
101
+ File.expand_path("../../fixtures/xmldsig/keys/rsa_private.pem", __dir__)
102
+ end
103
+ let(:fixture_key) { OpenSSL::PKey::RSA.new(File.read(pem_path)) }
104
+
105
+ it "loads and round-trips" do
106
+ skip "fixture key not present" unless File.exist?(pem_path)
107
+
108
+ doc = sign_and_attach(document_xml, fixture_key)
109
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: fixture_key)
110
+ expect(result.valid?).to be true
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XmlEmitter do
6
+ describe ".escape_text" do
7
+ it "escapes only & < > in a single pass" do
8
+ expect(described_class.escape_text("a & < > ' \""))
9
+ .to eq("a &amp; &lt; &gt; ' \"")
10
+ end
11
+
12
+ it "returns clean strings unallocated" do
13
+ input = "clean"
14
+ expect(described_class.escape_text(input)).to equal(input)
15
+ end
16
+
17
+ it "coerces non-strings" do
18
+ expect(described_class.escape_text(1)).to eq("1")
19
+ end
20
+ end
21
+
22
+ describe ".escape_attribute" do
23
+ it "escapes & < > and double quotes, keeping apostrophes literal" do
24
+ expect(described_class.escape_attribute(%q{& < > ' "}))
25
+ .to eq("&amp; &lt; &gt; ' &quot;")
26
+ end
27
+ end
28
+
29
+ describe ".cdata" do
30
+ it "wraps content in a CDATA section" do
31
+ expect(described_class.cdata("data")).to eq("<![CDATA[data]]>")
32
+ end
33
+
34
+ it "splits an embedded end sequence into adjacent sections" do
35
+ expect(described_class.cdata("a]]>b"))
36
+ .to eq("<![CDATA[a]]]]><![CDATA[>b]]>")
37
+ end
38
+ end
39
+
40
+ describe ".declaration_xml" do
41
+ it "renders only the set attributes" do
42
+ expect(described_class.declaration_xml("1.0", nil, nil))
43
+ .to eq('<?xml version="1.0"?>')
44
+ expect(described_class.declaration_xml("1.0", "UTF-8", "yes"))
45
+ .to eq('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
46
+ end
47
+
48
+ it "omits empty encodings" do
49
+ expect(described_class.declaration_xml("1.0", "", nil))
50
+ .to eq('<?xml version="1.0"?>')
51
+ end
52
+ end
53
+
54
+ describe ".doctype_xml" do
55
+ it "renders PUBLIC and SYSTEM forms and bare names" do
56
+ expect(described_class.doctype_xml("r", "pub", "sys"))
57
+ .to eq('<!DOCTYPE r PUBLIC "pub" "sys">')
58
+ expect(described_class.doctype_xml("r", nil, "sys"))
59
+ .to eq('<!DOCTYPE r SYSTEM "sys">')
60
+ expect(described_class.doctype_xml("r", nil, nil))
61
+ .to eq("<!DOCTYPE r>")
62
+ end
63
+ end
64
+ end
@@ -163,6 +163,78 @@ RSpec.describe "XPath Axes" do
163
163
  end
164
164
  end
165
165
 
166
+ describe "attribute axis with namespaces" do
167
+ let(:ns_doc) do
168
+ xml = <<~XML
169
+ <root xmlns:p="http://x.org" xmlns:q="http://x.org">
170
+ <item q:type="namespaced" type="bare" other="o"/>
171
+ </root>
172
+ XML
173
+ context.parse(xml)
174
+ end
175
+
176
+ def select(expression, namespaces = nil)
177
+ ast = Moxml::XPath::Parser.parse(expression)
178
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast,
179
+ namespaces: namespaces)
180
+ proc.call(ns_doc).map(&:value)
181
+ end
182
+
183
+ it "matches bare names against no-namespace attributes only" do
184
+ expect(select("/root/item/@type")).to eq(["bare"])
185
+ end
186
+
187
+ it "matches prefixed names by namespace URI, not by prefix spelling" do
188
+ expect(select("/root/item/@p:type")).to eq(["namespaced"])
189
+ expect(select("/root/item/@q:type")).to eq(["namespaced"])
190
+ end
191
+
192
+ it "matches any local name within a prefix's namespace" do
193
+ expect(select("/root/item/@q:*")).to eq(["namespaced"])
194
+ end
195
+
196
+ it "matches nothing for an undeclared prefix" do
197
+ expect(select("/root/item/@z:type")).to be_empty
198
+ end
199
+
200
+ it "prefers context prefix mappings over document declarations" do
201
+ expect(select("/root/item/@p:type", "p" => "http://other.org"))
202
+ .to be_empty
203
+ expect(select("/root/item/@zzz:type", "zzz" => "http://x.org"))
204
+ .to eq(["namespaced"])
205
+ end
206
+ end
207
+
208
+ describe "descendant-or-self shorthand from an element" do
209
+ it "selects descendants with .//step" do
210
+ ast = Moxml::XPath::Parser.parse(".//grandchild")
211
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
212
+ result = proc.call(doc.root.children.find(&:element?))
213
+
214
+ expect(result.map { |n| n.attribute("id").value })
215
+ .to contain_exactly("g1", "g2")
216
+ end
217
+ end
218
+
219
+ describe "node type tests as steps" do
220
+ it "selects text nodes with text()" do
221
+ ast = Moxml::XPath::Parser.parse("//grandchild/text()")
222
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
223
+ result = proc.call(doc)
224
+
225
+ expect(result.map(&:content)).to contain_exactly("text1", "text2")
226
+ end
227
+
228
+ it "selects comments with comment()" do
229
+ commented = context.parse("<root><!-- c1 --><a/><!-- c2 --></root>")
230
+ ast = Moxml::XPath::Parser.parse("//comment()")
231
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
232
+ result = proc.call(commented)
233
+
234
+ expect(result.map(&:content)).to contain_exactly(" c1 ", " c2 ")
235
+ end
236
+ end
237
+
166
238
  describe "descendant axis" do
167
239
  it "finds descendants without self" do
168
240
  ast = Moxml::XPath::Parser.parse("/root/descendant::child")
@@ -60,6 +60,12 @@ RSpec.describe Moxml::XPath::Parser do
60
60
  expect(ast.type).to eq(:relative_path)
61
61
  end
62
62
 
63
+ it "parses prefixed attribute names" do
64
+ ast = described_class.parse("@xmi:type")
65
+ expect(ast.type).to eq(:relative_path)
66
+ expect { described_class.parse("@xmi:*") }.not_to raise_error
67
+ end
68
+
63
69
  it "parses path with parent reference" do
64
70
  ast = described_class.parse("../book")
65
71
  expect(ast.type).to eq(:relative_path)
@@ -131,11 +131,13 @@ RSpec.describe "XPath Capabilities" do
131
131
  end
132
132
 
133
133
  describe "XPath functions" do
134
- it "supports count() function" do
134
+ it "supports count() function, returning the scalar unwrapped" do
135
135
  doc = Moxml.new.parse(simple_xml)
136
- # NOTE: count() returns a number, not nodes
137
136
  result = doc.xpath("count(//book)")
138
- expect(result).to be_a(Numeric) if result.is_a?(Numeric)
137
+ # REXML's XPath has no function support and yields an empty set
138
+ skip "count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
139
+
140
+ expect(result).to eq(2)
139
141
  rescue Moxml::XPathError, NoMethodError
140
142
  skip "count() function not supported on #{adapter_name}"
141
143
  end
@@ -23,14 +23,21 @@ RSpec.shared_examples "Performance Examples" do
23
23
  context "measures performance" do
24
24
  let(:doc) { context.parse(large_xml) }
25
25
 
26
+ # Absolute ips varies more than 2x across machines and even between
27
+ # runs on the same machine (observed: rexml serializer 1.5-2.9 ips on
28
+ # one machine). These thresholds sit at roughly half of typical
29
+ # modern-runner throughput so they only trip on asymptotic
30
+ # regressions (accidental O(n^2), broken caching), not on hardware
31
+ # variance or a few percent of legitimate overhead.
26
32
  let(:thresholds) do
27
33
  {
28
- nokogiri: { parser: 15, serializer: 1000 },
29
- oga: { parser: 10, serializer: 100 },
30
- rexml: { parser: 0, serializer: 5 },
31
- ox: { parser: 2, serializer: 1000 },
32
- headed_ox: { parser: 2, serializer: 1000 },
33
- libxml: { parser: 500, serializer: 60 },
34
+ nokogiri: { parser: 8, serializer: 500 },
35
+ oga: { parser: 5, serializer: 50 },
36
+ rexml: { parser: 0, serializer: 1 },
37
+ ox: { parser: 1, serializer: 400 },
38
+ headed_ox: { parser: 1, serializer: 400 },
39
+ libxml: { parser: 200, serializer: 30 },
40
+ leptris: { parser: 1300, serializer: 600 },
34
41
  }
35
42
  end
36
43
 
@@ -20,10 +20,6 @@ RSpec.shared_examples "Thread Safety Examples" do
20
20
  end
21
21
 
22
22
  it "handles concurrent processing" do
23
- if Moxml.new.config.adapter_name == :ox
24
- skip "Ox doesn't have a native XPath"
25
- end
26
-
27
23
  processor = processor_class.new
28
24
  threads = []
29
25
  results = Queue.new
data/spec/spec_helper.rb CHANGED
@@ -101,7 +101,11 @@ RSpec.configure do |config|
101
101
  end
102
102
 
103
103
  Moxml.configure do |config|
104
- config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::DEFAULT_ADAPTER
104
+ # Pin the suite baseline to the fallback adapter so results are
105
+ # reproducible regardless of whether the local environment has a
106
+ # leptris new enough to win the runtime-default probe. Leptris gets
107
+ # full coverage through its explicit adapter/integration matrices.
108
+ config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::FALLBACK_ADAPTER
105
109
  config.strict_parsing = true
106
110
  config.default_encoding = "UTF-8"
107
111
  config.entity_load_mode = :optional if RUBY_ENGINE == "opal"