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,25 @@
1
+ <?xml version="1.0"?>
2
+ <Envelope xmlns="urn:envelope">
3
+ <Data>
4
+ Hello, World!
5
+ </Data>
6
+ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
7
+ <SignedInfo>
8
+ <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9
+ <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
10
+ <Reference>
11
+ <Transforms>
12
+ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
13
+ <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
14
+ </Transforms>
15
+ <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
16
+ <DigestValue>Gx8CGUsbi2qvBLd15VCmwELbDMND8F4vY3jPOc7/FJ0=</DigestValue>
17
+ </Reference>
18
+ </SignedInfo>
19
+ <SignatureValue>T2c7nqOw55P8hcP1qhvfPCwOSEAuo8HstZf9shlrggcarxfgWTKhA6UdrF4McfrS
20
+ XtcgHA7zy0Yzd2cgeGkKA2jgI+9QRhoQsifOMuI55sE5r+fpBs+goaxC57gmcBXj
21
+ XnuwIiWf7nfpF4hYZ841HzYd2HcpQKPTdbhvZUprvx8=</SignatureValue>
22
+ <KeyInfo>
23
+ <KeyName>test</KeyName>
24
+ </KeyInfo>
25
+ </Signature></Envelope>
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0"?>
2
+ <Envelope xmlns="urn:envelope">
3
+ <Data>
4
+ Hello, World!
5
+ </Data>
6
+ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
7
+ <SignedInfo>
8
+ <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
9
+ <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
10
+ <Reference>
11
+ <Transforms>
12
+ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
13
+ <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
14
+ </Transforms>
15
+ <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
16
+ <DigestValue>Gx8CGUsbi2qvBLd15VCmwELbDMND8F4vY3jPOc7/FJ0=</DigestValue>
17
+ </Reference>
18
+ </SignedInfo>
19
+ <SignatureValue>TGJ9fCzjppp3LgG4fiBJx+0R34wRa7il9XKKZ+kkOAdKkcW0PIAYKmjn0Tn8krGd
20
+ Gw6qtFFqjdohXfhkKmajXAFunEtd3J0kHFkf3obIwRB1qdsYmKXVFxUx3GqcIlph
21
+ vt9v/9FC12JAxwAiJXHuY2xN5uo3xSDER4+tCCy3/AI=</SignatureValue>
22
+ <KeyInfo>
23
+ <X509Data>
24
+ <X509Certificate>MIICLzCCAZgCCQCVuhhQ38rw0TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJV
25
+ UzEQMA4GA1UECAwHR2VvcmdpYTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
26
+ dHkgTHRkMRcwFQYDVQQDDA53d3cuZ29vZ2xlLmNvbTAgFw0xMzA1MjUxODQwMDRa
27
+ GA8zMDEyMDkyNTE4NDAwNFowWzELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0dlb3Jn
28
+ aWExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEXMBUGA1UEAwwO
29
+ d3d3Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALE4oSql
30
+ eymfHtzOeY86WyvfsjZmaz2XnIo9dzZsK71yMEKkgvXQnnYy9pK0NaYcG0B0hcii
31
+ 3fqGBiHMkZY2BOGWwCC/wOmJCzLq9q6caPWUs71Zko+h59LaqV93vzDmZaXYfFoQ
32
+ gSVEWpEpCSo560x0mSuLnJYdQQzZ/L6xvxZ1AgMBAAEwDQYJKoZIhvcNAQEFBQAD
33
+ gYEATyK/RlfpohUVimgFkycTF2hyusjctseXoZDCctgg/STMsL8iA0P9YB6k91GC
34
+ kWpwevuiwarD1MfSUV6goPINFkIBvfK+5R9lpHaTqqs615z8T9R5VJgaLcFe3tWd
35
+ 7oq3V2q5Nl6MrZfXj2N07qe6/9zfdauxYO26vAEKCvIkbMo=
36
+ </X509Certificate>
37
+ </X509Data>
38
+ </KeyInfo>
39
+ </Signature></Envelope>
@@ -2,69 +2,87 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe Moxml::Adapter, ".platform_adapters" do
6
- it "includes all known adapters under MRI" do
7
- expect(described_class.platform_adapters).to include(:nokogiri, :oga,
8
- :rexml, :ox)
9
- end
10
-
11
- it "uses the AVAILABLE_ADAPTERS constant under MRI" do
12
- expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
13
- end
14
- end
15
-
16
- RSpec.describe Moxml::Adapter, ".available?" do
17
- it "returns true for :oga" do
18
- expect(described_class.available?(:oga)).to be true
19
- end
5
+ RSpec.describe Moxml::Adapter do
6
+ describe ".platform_adapters" do
7
+ it "includes all known adapters under MRI" do
8
+ expect(described_class.platform_adapters).to include(:nokogiri, :oga,
9
+ :rexml, :ox)
10
+ end
20
11
 
21
- it "returns true for :nokogiri under MRI" do
22
- expect(described_class.available?(:nokogiri)).to be true
12
+ it "uses the AVAILABLE_ADAPTERS constant under MRI" do
13
+ expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
14
+ end
23
15
  end
24
16
 
25
- context "when Opal platform adapters are in effect" do
26
- before do
27
- allow(described_class).to receive(:platform_adapters)
28
- .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
17
+ describe ".available?" do
18
+ it "returns true for :oga" do
19
+ expect(described_class.available?(:oga)).to be true
29
20
  end
30
21
 
31
- it "returns false for :nokogiri" do
32
- expect(described_class.available?(:nokogiri)).to be false
22
+ it "returns true for :nokogiri under MRI" do
23
+ expect(described_class.available?(:nokogiri)).to be true
33
24
  end
34
25
 
35
- it "returns true for :rexml" do
36
- expect(described_class.available?(:rexml)).to be true
26
+ context "when Opal platform adapters are in effect" do
27
+ before do
28
+ allow(described_class).to receive(:platform_adapters)
29
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
30
+ end
31
+
32
+ it "returns false for :nokogiri" do
33
+ expect(described_class.available?(:nokogiri)).to be false
34
+ end
35
+
36
+ it "returns true for :oga" do
37
+ expect(described_class.available?(:oga)).to be true
38
+ end
39
+
40
+ it "returns true for :rexml" do
41
+ expect(described_class.available?(:rexml)).to be true
42
+ end
37
43
  end
38
44
  end
39
- end
40
45
 
41
- RSpec.describe Moxml::Adapter, ".load" do
42
- context "when Opal platform adapters are in effect" do
43
- before do
44
- allow(described_class).to receive(:platform_adapters)
45
- .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
46
- end
46
+ describe ".load" do
47
+ context "when Opal platform adapters are in effect" do
48
+ before do
49
+ allow(described_class).to receive(:platform_adapters)
50
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
51
+ end
47
52
 
48
- it "raises AdapterError for :nokogiri" do
49
- expect { described_class.load(:nokogiri) }.to raise_error(
50
- Moxml::AdapterError, /not available on this platform/
51
- )
53
+ it "raises AdapterError for :nokogiri" do
54
+ expect { described_class.load(:nokogiri) }.to raise_error(
55
+ Moxml::AdapterError, /not available on this platform/
56
+ )
57
+ end
52
58
  end
53
59
  end
54
- end
55
60
 
56
- RSpec.describe "Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS" do
57
- it "contains only :rexml" do
58
- expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[rexml])
61
+ describe "OPAL_AVAILABLE_ADAPTERS" do
62
+ it "lists :oga as the primary Opal adapter, with :rexml as opt-in" do
63
+ expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[oga rexml])
64
+ end
59
65
  end
60
- end
61
66
 
62
- RSpec.describe "Moxml::Adapter::CONST_NAME_MAP" do
63
- it "maps :headed_ox to HeadedOx" do
64
- expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
67
+ describe "CONST_NAME_MAP" do
68
+ it "maps :headed_ox to HeadedOx" do
69
+ expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
70
+ end
71
+
72
+ it "falls back to capitalize for unmapped adapters" do
73
+ expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
74
+ end
65
75
  end
66
76
 
67
- it "falls back to capitalize for unmapped adapters" do
68
- expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
77
+ describe "default adapter / available list invariants" do
78
+ it "DEFAULT_ADAPTER is a member of AVAILABLE_ADAPTERS" do
79
+ expect(Moxml::Adapter::AVAILABLE_ADAPTERS)
80
+ .to include(Moxml::Config::DEFAULT_ADAPTER)
81
+ end
82
+
83
+ it "OPAL_DEFAULT_ADAPTER is a member of OPAL_AVAILABLE_ADAPTERS" do
84
+ expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
85
+ .to include(Moxml::Config::OPAL_DEFAULT_ADAPTER)
86
+ end
69
87
  end
70
88
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ RSpec.describe "Moxml::C14n API" do
7
+ let(:ctx) { Moxml.new(:nokogiri) }
8
+
9
+ describe ".canonicalize (algorithm selector)" do
10
+ it "defaults to inclusive C14N 1.0" do
11
+ doc = ctx.parse("<root xmlns:foo='urn:foo'><foo:a/></root>")
12
+ default_result = Moxml::C14n.canonicalize(doc.root)
13
+ incl10_result = Moxml::C14n.canonicalize(doc.root, algorithm: :inclusive10)
14
+ expect(default_result).to eq(incl10_result)
15
+ end
16
+
17
+ it "raises ArgumentError for unknown algorithm" do
18
+ expect do
19
+ Moxml::C14n.canonicalize("<x/>", algorithm: :bogus)
20
+ end.to raise_error(ArgumentError, /unknown C14N algorithm/)
21
+ end
22
+
23
+ it "accepts inclusive_11 algorithm" do
24
+ doc = ctx.parse("<root/>")
25
+ result = Moxml::C14n.canonicalize(doc.root, algorithm: :inclusive11)
26
+ expect(result).to eq("<root></root>")
27
+ end
28
+
29
+ it "accepts exclusive_10 algorithm" do
30
+ doc = ctx.parse("<root xmlns:foo='urn:foo'><foo:a/></root>")
31
+ result = Moxml::C14n.canonicalize(doc.root, algorithm: :exclusive10)
32
+ # Exclusive renders ns where visibly used (on foo:a), not on root
33
+ expect(result).to include('<foo:a xmlns:foo="urn:foo">')
34
+ end
35
+ end
36
+
37
+ describe ".equivalent?" do
38
+ it "returns true for byte-identical inputs" do
39
+ expect(Moxml::C14n).to be_equivalent("<root>x</root>", "<root>x</root>")
40
+ end
41
+
42
+ it "returns true when only attribute whitespace differs" do
43
+ # Whitespace inside the tag is not significant — both parse identically.
44
+ a = %(<root attr="value"/>)
45
+ b = %(<root attr="value" />)
46
+ expect(Moxml::C14n).to be_equivalent(a, b)
47
+ end
48
+
49
+ it "returns false when text whitespace differs (text IS significant)" do
50
+ a = "<root><child>a</child> </root>"
51
+ b = "<root> <child>a</child></root>"
52
+ expect(Moxml::C14n).not_to be_equivalent(a, b)
53
+ end
54
+
55
+ it "returns true when only attribute order differs" do
56
+ a = %(<root a="1" b="2"/>)
57
+ b = %(<root b="2" a="1"/>)
58
+ expect(Moxml::C14n).to be_equivalent(a, b)
59
+ end
60
+
61
+ it "returns true when only namespace prefix differs" do
62
+ a = "<root xmlns:foo='urn:foo'><foo:a/></root>"
63
+ b = "<root xmlns:bar='urn:foo'><bar:a/></root>"
64
+ # Inclusive canonical form: prefix is part of qname → not equivalent
65
+ expect(Moxml::C14n).not_to be_equivalent(a, b)
66
+ end
67
+
68
+ it "returns false for different content" do
69
+ expect(Moxml::C14n).not_to be_equivalent("<root>a</root>", "<root>b</root>")
70
+ end
71
+
72
+ it "respects algorithm argument" do
73
+ a = "<root xmlns:foo='urn:foo'><foo:a/></root>"
74
+ b = "<root><a xmlns:foo='urn:foo'/></root>"
75
+ # Inclusive: ns on root vs on child → different
76
+ expect(Moxml::C14n).not_to be_equivalent(a, b, algorithm: :inclusive10)
77
+ end
78
+
79
+ it "accepts Moxml::Node inputs" do
80
+ doc1 = ctx.parse("<root/>")
81
+ doc2 = ctx.parse("<root></root>")
82
+ expect(Moxml::C14n).to be_equivalent(doc1.root, doc2.root)
83
+ end
84
+ end
85
+
86
+ describe "convenience methods" do
87
+ it ".canonicalize_inclusive10 matches default" do
88
+ doc = ctx.parse("<root/>")
89
+ expect(Moxml::C14n.canonicalize_inclusive10(doc.root))
90
+ .to eq(Moxml::C14n.canonicalize(doc.root))
91
+ end
92
+
93
+ it ".canonicalize_inclusive11 produces output" do
94
+ doc = ctx.parse("<root/>")
95
+ expect(Moxml::C14n.canonicalize_inclusive11(doc.root)).to eq("<root></root>")
96
+ end
97
+
98
+ it ".canonicalize_exclusive accepts inclusive_namespaces" do
99
+ doc = ctx.parse("<root xmlns:foo='urn:foo'/>")
100
+ result = Moxml::C14n.canonicalize_exclusive(doc.root, inclusive_namespaces: ["foo"])
101
+ # Even though foo is not visibly used, inclusive list forces render
102
+ expect(result).to include('xmlns:foo="urn:foo"')
103
+ end
104
+ end
105
+
106
+ describe "escape helpers (backward compat)" do
107
+ it ".escape_text escapes & < >" do
108
+ expect(Moxml::C14n.escape_text("a & b < c > d"))
109
+ .to eq("a &amp; b &lt; c &gt; d")
110
+ end
111
+
112
+ it ".escape_attribute escapes quotes and whitespace" do
113
+ expect(Moxml::C14n.escape_attribute(%(a"b\tc\nd)))
114
+ .to eq(%(a&quot;b&#x9;c&#xA;d))
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+
6
+ RSpec.describe "Moxml C14N engine" do
7
+ describe Moxml::C14n::Exclusive do
8
+ let(:ctx) { Moxml.new(:nokogiri) }
9
+ let(:canon) { described_class.new }
10
+
11
+ it "renders a simple element without namespaces" do
12
+ doc = ctx.parse("<root>hello</root>")
13
+ expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
14
+ end
15
+
16
+ it "preserves nested element structure" do
17
+ doc = ctx.parse("<root><child>text</child></root>")
18
+ expect(canon.canonicalize(doc.root)).to eq("<root><child>text</child></root>")
19
+ end
20
+
21
+ it "renders declared namespaces" do
22
+ doc = ctx.parse(<<~XML.strip)
23
+ <root xmlns:foo="http://example.com/foo"><foo:child>x</foo:child></root>
24
+ XML
25
+ result = canon.canonicalize(doc.root)
26
+ # Exclusive C14N renders the namespace on the element where it's visibly
27
+ # used (foo:child), not on ancestors that don't use it.
28
+ expect(result).to include('<foo:child xmlns:foo="http://example.com/foo">')
29
+ expect(result).to include("</foo:child>")
30
+ end
31
+
32
+ it "excludes ancestor-inherited namespaces when not visibly used (exclusive)" do
33
+ doc = ctx.parse(<<~XML.strip)
34
+ <outer xmlns:unused="http://example.com/unused"><inner>text</inner></outer>
35
+ XML
36
+ inner = doc.at_xpath("//inner")
37
+ result = canon.canonicalize(inner)
38
+ expect(result).to eq("<inner>text</inner>")
39
+ end
40
+
41
+ it "escapes special characters in text content" do
42
+ doc = ctx.parse("<root>a&lt;b&gt;c&amp;d</root>")
43
+ expect(canon.canonicalize(doc.root)).to eq("<root>a&lt;b&gt;c&amp;d</root>")
44
+ end
45
+
46
+ it "escapes special characters in attribute values" do
47
+ doc = ctx.parse(%(<root attr="a&amp;b&quot;c"/>))
48
+ expect(canon.canonicalize(doc.root))
49
+ .to include(%(attr="a&amp;b&quot;c"))
50
+ end
51
+
52
+ it "sorts attributes by namespace URI then local name" do
53
+ doc = ctx.parse(%(<root b="2" a="1" c="3"/>))
54
+ result = canon.canonicalize(doc.root)
55
+ expect(result).to include('a="1" b="2" c="3"')
56
+ end
57
+
58
+ it "omits comments by default" do
59
+ doc = ctx.parse("<root>a<!-- comment -->b</root>")
60
+ expect(canon.canonicalize(doc.root)).to eq("<root>ab</root>")
61
+ end
62
+
63
+ it "includes comments when with_comments is true" do
64
+ doc = ctx.parse("<root>a<!-- comment -->b</root>")
65
+ expect(canon.canonicalize(doc.root, with_comments: true))
66
+ .to eq("<root>a<!-- comment -->b</root>")
67
+ end
68
+ end
69
+
70
+ describe ".escape_text" do
71
+ it "escapes &, <, >" do
72
+ expect(Moxml::C14n.escape_text("a & b < c > d"))
73
+ .to eq("a &amp; b &lt; c &gt; d")
74
+ end
75
+
76
+ it "escapes bare CR as &#xD;" do
77
+ expect(Moxml::C14n.escape_text("a\rb"))
78
+ .to eq("a&#xD;b")
79
+ end
80
+ end
81
+
82
+ describe ".escape_attribute" do
83
+ it "escapes quotes and tabs/newlines" do
84
+ expect(Moxml::C14n.escape_attribute(%(a"b\tc\nd)))
85
+ .to eq(%(a&quot;b&#x9;c&#xA;d))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ # Edge cases for comment and processing-instruction handling per
7
+ # W3C C14N 1.0 §2.6, §2.7.
8
+ RSpec.describe "C14N comments and PIs" do
9
+ let(:ctx) { Moxml.new(:nokogiri) }
10
+
11
+ describe "comments inside elements" do
12
+ it "omits comments by default" do
13
+ doc = ctx.parse("<root>a<!-- c -->b</root>")
14
+ expect(Moxml::C14n.canonicalize(doc.root)).to eq("<root>ab</root>")
15
+ end
16
+
17
+ it "includes comments when with_comments: true" do
18
+ doc = ctx.parse("<root>a<!-- c -->b</root>")
19
+ expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
20
+ .to eq("<root>a<!-- c -->b</root>")
21
+ end
22
+
23
+ it "preserves comment content verbatim" do
24
+ doc = ctx.parse("<root><!-- &amp; <stuff> --></root>")
25
+ expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
26
+ .to eq("<root><!-- &amp; <stuff> --></root>")
27
+ end
28
+ end
29
+
30
+ describe "processing instructions" do
31
+ it "always renders PIs (not affected by with_comments)" do
32
+ doc = ctx.parse("<root><?pi data?>x</root>")
33
+ expect(Moxml::C14n.canonicalize(doc.root))
34
+ .to eq("<root><?pi data?>x</root>")
35
+ end
36
+
37
+ it "renders PI without data when data is empty" do
38
+ doc = ctx.parse("<root><?pi?>x</root>")
39
+ expect(Moxml::C14n.canonicalize(doc.root))
40
+ .to eq("<root><?pi?>x</root>")
41
+ end
42
+
43
+ it "preserves PI target and data verbatim" do
44
+ doc = ctx.parse(%(<root><?xml-stylesheet href="a.css" type="text/css"?>x</root>))
45
+ expect(Moxml::C14n.canonicalize(doc.root))
46
+ .to include(%(<?xml-stylesheet href="a.css" type="text/css"?>))
47
+ end
48
+ end
49
+
50
+ describe "PIs at document level" do
51
+ # TODO.c14n/09: Document-level PIs (outside the document element) are
52
+ # not yet rendered correctly. The moxml PI parser mis-parses document-level
53
+ # PIs, and the DataModel.from_document path drops them. Fixing requires
54
+ # adapter-level work and is tracked separately.
55
+ it "renders PIs outside the document element (KNOWN LIMITATION)" do
56
+ skip "document-level PI rendering not yet implemented"
57
+ end
58
+ end
59
+
60
+ describe "comments at document level" do
61
+ it "renders document-level comments when with_comments: true (KNOWN LIMITATION)" do
62
+ skip "document-level comment rendering not yet implemented"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+
6
+ RSpec.describe Moxml::C14n::Inclusive10 do
7
+ let(:ctx) { Moxml.new(:nokogiri) }
8
+ let(:canon) { described_class.new }
9
+
10
+ it "renders a simple element" do
11
+ doc = ctx.parse("<root>hello</root>")
12
+ expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
13
+ end
14
+
15
+ it "attracts ancestor namespaces at the apex (the key inclusive behavior)" do
16
+ xml = <<~XML.strip
17
+ <outer xmlns:foo="http://example.com/foo" xmlns:bar="http://example.com/bar">
18
+ <inner x="1"><foo:child>hello</foo:child></inner>
19
+ </outer>
20
+ XML
21
+ doc = ctx.parse(xml)
22
+ inner = doc.at_xpath("//*[local-name()='inner']")
23
+ result = canon.canonicalize(inner)
24
+ # Both inherited namespaces are rendered on <inner>, even though
25
+ # <inner> itself does not visibly use them. This is the inclusive
26
+ # "ancestor attraction" behavior.
27
+ expect(result).to include('xmlns:bar="http://example.com/bar"')
28
+ expect(result).to include('xmlns:foo="http://example.com/foo"')
29
+ end
30
+
31
+ it "renders each namespace declaration only once per chain" do
32
+ xml = <<~XML.strip
33
+ <root xmlns:a="http://example.com/a">
34
+ <child xmlns:a="http://example.com/a"><grand/></child>
35
+ </root>
36
+ XML
37
+ doc = ctx.parse(xml)
38
+ result = canon.canonicalize(doc.root)
39
+ expect(result.scan('xmlns:a="http://example.com/a"').count).to eq(1)
40
+ end
41
+
42
+ it "re-renders a namespace when re-declared with a different URI" do
43
+ xml = <<~XML.strip
44
+ <root xmlns:a="http://example.com/a">
45
+ <child xmlns:a="http://example.com/other"><grand/></child>
46
+ </root>
47
+ XML
48
+ doc = ctx.parse(xml)
49
+ result = canon.canonicalize(doc.root)
50
+ expect(result.scan("xmlns:a=").count).to eq(2)
51
+ end
52
+
53
+ it "renders the default namespace at apex when inherited" do
54
+ xml = '<outer xmlns="http://example.com"><inner/></outer>'
55
+ doc = ctx.parse(xml)
56
+ inner = doc.at_xpath("//*[local-name()='inner']")
57
+ result = canon.canonicalize(inner)
58
+ expect(result).to include('xmlns="http://example.com"')
59
+ end
60
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ # Edge cases for namespace rendering per W3C C14N 1.0 §2.3 / 1.1 §2.3.
7
+ # These cover the bugs that historically plague C14N implementations.
8
+ RSpec.describe "C14N namespace rendering" do
9
+ let(:ctx) { Moxml.new(:nokogiri) }
10
+
11
+ describe "default namespace" do
12
+ it "renders xmlns= declaration when default ns is set" do
13
+ doc = ctx.parse(%(<root xmlns="urn:foo"/>))
14
+ result = Moxml::C14n.canonicalize(doc.root)
15
+ expect(result).to eq('<root xmlns="urn:foo"></root>')
16
+ end
17
+
18
+ it "renders xmlns='' when transitioning to empty default" do
19
+ doc = ctx.parse(%(<a xmlns="urn:foo"><b xmlns=""><c/></b></a>))
20
+ result = Moxml::C14n.canonicalize(doc.root)
21
+ # <b> has empty default; <a> has urn:foo → xmlns="" appears
22
+ expect(result).to include('xmlns=""')
23
+ end
24
+
25
+ it "does not render xmlns when default ns is empty everywhere" do
26
+ doc = ctx.parse(%(<root><child/></root>))
27
+ result = Moxml::C14n.canonicalize(doc.root)
28
+ expect(result).to eq("<root><child></child></root>")
29
+ end
30
+ end
31
+
32
+ describe "prefix redeclaration" do
33
+ it "renders both declarations when URI changes" do
34
+ doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:2"/></a>))
35
+ result = Moxml::C14n.canonicalize(doc.root)
36
+ expect(result.scan("xmlns:x=").length).to eq(2)
37
+ end
38
+
39
+ it "renders only once when URI is unchanged" do
40
+ doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:1"/></a>))
41
+ result = Moxml::C14n.canonicalize(doc.root)
42
+ expect(result.scan("xmlns:x=").length).to eq(1)
43
+ end
44
+ end
45
+
46
+ describe "xml namespace" do
47
+ it "does not render xmlns:xml declaration" do
48
+ doc = ctx.parse(%(<root xmlns:xml="http://www.w3.org/XML/1998/namespace"/>))
49
+ result = Moxml::C14n.canonicalize(doc.root)
50
+ expect(result).not_to include("xmlns:xml=")
51
+ end
52
+ end
53
+
54
+ describe "namespace sorting" do
55
+ it "sorts xmlns declarations by prefix" do
56
+ doc = ctx.parse(%(<root xmlns:z="urn:z" xmlns:a="urn:a"/>))
57
+ result = Moxml::C14n.canonicalize(doc.root)
58
+ # a should appear before z
59
+ expect(result.index("xmlns:a=")).to be < result.index("xmlns:z=")
60
+ end
61
+
62
+ it "renders default namespace first" do
63
+ doc = ctx.parse(%(<root xmlns="urn:default" xmlns:a="urn:a"/>))
64
+ result = Moxml::C14n.canonicalize(doc.root)
65
+ # Default (xmlns=) before prefixed
66
+ expect(result.index("xmlns=")).to be < result.index("xmlns:a=")
67
+ end
68
+ end
69
+
70
+ describe "deeply nested inheritance" do
71
+ it "renders each prefix at the level it is first declared" do
72
+ xml = <<~XML.strip
73
+ <root xmlns:a="urn:a">
74
+ <level1 xmlns:b="urn:b">
75
+ <level2 xmlns:c="urn:c">
76
+ <apex/>
77
+ </level2>
78
+ </level1>
79
+ </root>
80
+ XML
81
+ doc = ctx.parse(xml)
82
+ result = Moxml::C14n.canonicalize(doc.root)
83
+ # Inclusive: all in-scope ns render on apex. But we're canonicalizing
84
+ # the whole tree, so each ns renders at its declaration point.
85
+ expect(result.scan("xmlns:").length).to eq(3)
86
+ end
87
+ end
88
+ end