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