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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Element node in the C14N data model.
7
+ class ElementNode < Node
8
+ attr_reader :name, :namespace_uri, :prefix, :namespace_nodes,
9
+ :attribute_nodes
10
+
11
+ def initialize(name:, namespace_uri: nil, prefix: nil)
12
+ super()
13
+ @name = name
14
+ @namespace_uri = namespace_uri
15
+ @prefix = prefix
16
+ @namespace_nodes = []
17
+ @attribute_nodes = []
18
+ end
19
+
20
+ def node_type
21
+ :element
22
+ end
23
+
24
+ def qname
25
+ prefix.nil? || prefix.empty? ? name : "#{prefix}:#{name}"
26
+ end
27
+
28
+ def add_namespace(namespace_node)
29
+ namespace_node.parent = self
30
+ @namespace_nodes << namespace_node
31
+ end
32
+
33
+ def add_attribute(attribute_node)
34
+ attribute_node.parent = self
35
+ @attribute_nodes << attribute_node
36
+ end
37
+
38
+ # Namespace nodes sorted lexicographically by local name (prefix).
39
+ # Per W3C C14N 1.0 §2.3 / 1.1 §2.3, default namespace sorts first.
40
+ def sorted_namespace_nodes
41
+ @namespace_nodes.sort_by { |ns| ns.local_name.to_s }
42
+ end
43
+
44
+ # Attribute nodes sorted by namespace URI then local name
45
+ # (W3C C14N 1.0 §2.4 / 1.1 §2.4).
46
+ def sorted_attribute_nodes
47
+ @attribute_nodes.sort_by do |attr|
48
+ [attr.namespace_uri.to_s, attr.local_name]
49
+ end
50
+ end
51
+
52
+ def text_content
53
+ children.map(&:text_content).join
54
+ end
55
+
56
+ def to_s
57
+ "<#{qname}>"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Namespace node. Prefix is empty string for the default namespace.
7
+ class NamespaceNode < Node
8
+ attr_reader :prefix, :uri
9
+
10
+ def initialize(prefix:, uri:)
11
+ super()
12
+ @prefix = prefix
13
+ @uri = uri
14
+ end
15
+
16
+ def name
17
+ prefix.to_s
18
+ end
19
+
20
+ def node_type
21
+ :namespace
22
+ end
23
+
24
+ # Local name is the prefix (empty string for default namespace).
25
+ # Used by ElementNode#sorted_namespace_nodes for sort order.
26
+ def local_name
27
+ prefix.to_s
28
+ end
29
+
30
+ def default_namespace?
31
+ prefix.nil? || prefix.empty?
32
+ end
33
+
34
+ # The `xml` namespace is implicit and never rendered.
35
+ def xml_namespace?
36
+ prefix == "xml" && uri == Moxml::C14n::XML_URI
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Processing Instruction node.
7
+ class ProcessingInstructionNode < Node
8
+ attr_reader :target, :data
9
+
10
+ def initialize(target:, data: "")
11
+ super()
12
+ @target = target
13
+ @data = data
14
+ end
15
+
16
+ def name
17
+ target
18
+ end
19
+
20
+ def node_type
21
+ :processing_instruction
22
+ end
23
+
24
+ def text_content
25
+ ""
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Root node representing the document root.
7
+ class RootNode < Node
8
+ def name
9
+ "#document"
10
+ end
11
+
12
+ def node_type
13
+ :root
14
+ end
15
+
16
+ def children=(new_children)
17
+ @children = new_children
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Text node. Stores the decoded value (entity refs resolved).
7
+ class TextNode < Node
8
+ attr_accessor :value
9
+
10
+ def initialize(value:)
11
+ super()
12
+ @value = value
13
+ end
14
+
15
+ def name
16
+ "#text"
17
+ end
18
+
19
+ def node_type
20
+ :text
21
+ end
22
+
23
+ def text_content
24
+ @value
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # C14N data-model node types. All nodes inherit from {Moxml::C14n::Node}.
6
+ module Nodes
7
+ autoload :AttributeNode, "moxml/c14n/nodes/attribute_node"
8
+ autoload :CommentNode, "moxml/c14n/nodes/comment_node"
9
+ autoload :ElementNode, "moxml/c14n/nodes/element_node"
10
+ autoload :NamespaceNode, "moxml/c14n/nodes/namespace_node"
11
+ autoload :ProcessingInstructionNode,
12
+ "moxml/c14n/nodes/processing_instruction_node"
13
+ autoload :RootNode, "moxml/c14n/nodes/root_node"
14
+ autoload :TextNode, "moxml/c14n/nodes/text_node"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # C14N 1.0/1.1 processor. Walks the data model and emits canonical
6
+ # octets. Handles node-set subsets (omitted ancestors), xml:base
7
+ # fixup, and xml:* inheritable attribute resolution.
8
+ #
9
+ # Ported from canon (lutaml/canon).
10
+ class Processor
11
+ attr_reader :with_comments
12
+
13
+ def initialize(with_comments: false)
14
+ @with_comments = with_comments
15
+ @encoder = CharacterEncoder.new
16
+ @namespace_handler = NamespaceHandler.new(@encoder)
17
+ @attribute_handler = AttributeHandler.new(@encoder)
18
+ @xml_base_handler = XmlBaseHandler.new
19
+ end
20
+
21
+ def process(root_node)
22
+ output = (+"")
23
+ process_node(root_node, output)
24
+ output
25
+ end
26
+
27
+ private
28
+
29
+ def process_node(node, output, parent_element = nil, omitted_ancestors = [])
30
+ case node.node_type
31
+ when :root
32
+ node.children.each { |child| process_node(child, output) }
33
+ when :element
34
+ process_element_node(node, output, parent_element, omitted_ancestors)
35
+ when :text
36
+ process_text_node(node, output)
37
+ when :comment
38
+ process_comment_node(node, output, parent_element)
39
+ when :processing_instruction
40
+ process_pi_node(node, output, parent_element)
41
+ end
42
+ end
43
+
44
+ def process_element_node(node, output, parent_element, omitted_ancestors)
45
+ if node.in_node_set?
46
+ render_element(node, output, parent_element, omitted_ancestors)
47
+ else
48
+ # Element not in node-set, but its children may be. Pass the
49
+ # element as an omitted ancestor for inheritable-attr fixup.
50
+ new_omitted = omitted_ancestors + [node]
51
+ node.children.each do |child|
52
+ process_node(child, output, parent_element, new_omitted)
53
+ end
54
+ end
55
+ end
56
+
57
+ def render_element(node, output, parent_element, omitted_ancestors)
58
+ output << "<" << node.qname
59
+
60
+ @namespace_handler.process_namespaces(node, output, parent_element)
61
+ process_element_attributes(node, output, omitted_ancestors)
62
+
63
+ output << ">"
64
+
65
+ node.children.each { |child| process_node(child, output, node, []) }
66
+
67
+ output << "</" << node.qname << ">"
68
+ end
69
+
70
+ def process_element_attributes(node, output, omitted_ancestors)
71
+ @attribute_handler.process_attributes(node, output, omitted_ancestors)
72
+
73
+ return unless omitted_ancestors.any?
74
+
75
+ fixed_base = @xml_base_handler.fixup_xml_base(node, omitted_ancestors)
76
+ return unless fixed_base && !fixed_base.empty?
77
+
78
+ has_base = node.attribute_nodes.any?(&:xml_base?)
79
+ return if has_base
80
+
81
+ output << ' xml:base="'
82
+ output << @encoder.encode_attribute(fixed_base)
83
+ output << '"'
84
+ end
85
+
86
+ def process_text_node(node, output)
87
+ return unless node.in_node_set?
88
+
89
+ output << @encoder.encode_text(node.value)
90
+ end
91
+
92
+ def process_comment_node(node, output, parent_element)
93
+ return unless with_comments
94
+ return unless node.in_node_set?
95
+
96
+ # Comment outside the document element gets a line break before/after
97
+ # to keep canonical output readable.
98
+ if parent_element.nil? && output.length.positive?
99
+ output << "\n"
100
+ end
101
+ output << "<!--" << node.value << "-->"
102
+ output << "\n" if parent_element.nil?
103
+ end
104
+
105
+ def process_pi_node(node, output, parent_element)
106
+ return unless node.in_node_set?
107
+
108
+ if parent_element.nil? && output.length.positive?
109
+ output << "\n"
110
+ end
111
+ output << "<?" << node.target
112
+ output << " " << node.data unless node.data.to_s.empty?
113
+ output << "?>"
114
+ output << "\n" if parent_element.nil?
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Builds canonical octet output incrementally.
6
+ # Output is always UTF-8 with no BOM.
7
+ class Writer
8
+ attr_reader :output
9
+
10
+ def initialize
11
+ @output = (+"")
12
+ # Tracks namespaces already rendered in the output ancestor chain,
13
+ # so descendants don't re-render the same declaration.
14
+ # Key: element (by object identity), value: { prefix => uri }.
15
+ @rendered_stack = []
16
+ end
17
+
18
+ def <<(str)
19
+ @output << str
20
+ self
21
+ end
22
+
23
+ def raw(str)
24
+ @output << str.to_s
25
+ self
26
+ end
27
+
28
+ # Push a fresh "rendered" frame for the given element. Called by the
29
+ # canonicalizer before rendering the element's namespaces.
30
+ def open_rendered_frame(element)
31
+ parent_rendered = @rendered_stack.last || {}
32
+ # Inherit parent's rendered namespaces as the starting set.
33
+ @rendered_stack.push(parent_rendered.dup)
34
+ element
35
+ end
36
+
37
+ def close_rendered_frame
38
+ @rendered_stack.pop
39
+ self
40
+ end
41
+
42
+ def rendered_namespaces_for(_element)
43
+ @rendered_stack.last || {}
44
+ end
45
+
46
+ def mark_rendered(_element, prefix, uri)
47
+ (@rendered_stack.last || {})[prefix] = uri
48
+ self
49
+ end
50
+
51
+ def open_tag(prefix, local)
52
+ @output << "<"
53
+ @output << "#{prefix}:" unless prefix.nil? || prefix.empty?
54
+ @output << local
55
+ self
56
+ end
57
+
58
+ def close_tag_open
59
+ @output << ">"
60
+ self
61
+ end
62
+
63
+ def close_tag_self_close
64
+ @output << "></"
65
+ self
66
+ end
67
+
68
+ def close_tag(prefix, local)
69
+ @output << "</"
70
+ @output << "#{prefix}:" unless prefix.nil? || prefix.empty?
71
+ @output << local
72
+ @output << ">"
73
+ self
74
+ end
75
+
76
+ def attribute(expanded_name, value)
77
+ @output << " "
78
+ @output << expanded_name
79
+ @output << "=\""
80
+ @output << C14n.escape_attribute(value)
81
+ @output << "\""
82
+ self
83
+ end
84
+
85
+ def namespace(prefix, uri)
86
+ @output << " "
87
+ if prefix.nil? || prefix.empty?
88
+ @output << "xmlns"
89
+ else
90
+ @output << "xmlns:"
91
+ @output << prefix
92
+ end
93
+ @output << "=\""
94
+ @output << C14n.escape_attribute(uri)
95
+ @output << "\""
96
+ self
97
+ end
98
+
99
+ def text(content)
100
+ @output << C14n.escape_text(content)
101
+ self
102
+ end
103
+
104
+ def comment(content)
105
+ @output << "<!--"
106
+ @output << C14n.escape_text(content)
107
+ @output << "-->"
108
+ self
109
+ end
110
+
111
+ def processing_instruction(target, content)
112
+ @output << "<?"
113
+ @output << target
114
+ @output << " "
115
+ @output << content
116
+ @output << "?>"
117
+ self
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Moxml
6
+ module C14n
7
+ # xml:base fixup handler for document subsets (C14N 1.1 §2.4).
8
+ # Implements RFC 3986 URI joining with C14N 1.1 modifications.
9
+ class XmlBaseHandler
10
+ # Returns the fixed-up xml:base value to emit on the element, or
11
+ # nil if no fixup is needed.
12
+ def fixup_xml_base(element, omitted_ancestors)
13
+ return nil if omitted_ancestors.empty?
14
+
15
+ base_values = collect_base_values(element, omitted_ancestors)
16
+ return nil if base_values.empty?
17
+
18
+ join_base_values(base_values)
19
+ end
20
+
21
+ private
22
+
23
+ def collect_base_values(element, omitted_ancestors)
24
+ values = []
25
+ omitted_ancestors.each do |ancestor|
26
+ base_attr = ancestor.attribute_nodes.find(&:xml_base?)
27
+ values << base_attr.value if base_attr
28
+ end
29
+ element_base = element.attribute_nodes.find(&:xml_base?)
30
+ values << element_base.value if element_base
31
+ values
32
+ end
33
+
34
+ def join_base_values(values)
35
+ result = values.first
36
+ values[1..].each { |ref| result = join_uri_references(result, ref) }
37
+ result
38
+ end
39
+
40
+ # Join two URI references per RFC 3986 §5.2.1–5.2.4 with the
41
+ # C14N 1.1 modification (drop fragment).
42
+ def join_uri_references(base, ref)
43
+ ref_parts = parse_uri(ref)
44
+ return remove_dot_segments(ref_parts[:path] || "") if ref_parts[:scheme]
45
+
46
+ base_parts = parse_uri(base)
47
+ result_parts = {}
48
+
49
+ if ref_parts[:authority]
50
+ result_parts[:authority] = ref_parts[:authority]
51
+ result_parts[:path] = remove_dot_segments(ref_parts[:path] || "")
52
+ result_parts[:query] = ref_parts[:query]
53
+ else
54
+ if ref_parts[:path].nil? || ref_parts[:path].empty?
55
+ result_parts[:path] = base_parts[:path]
56
+ result_parts[:query] = ref_parts[:query] || base_parts[:query]
57
+ elsif ref_parts[:path].start_with?("/")
58
+ result_parts[:path] = remove_dot_segments(ref_parts[:path])
59
+ else
60
+ result_parts[:path] = remove_dot_segments(
61
+ merge_paths(base_parts[:path], ref_parts[:path]),
62
+ )
63
+ end
64
+ result_parts[:query] = ref_parts[:query]
65
+ result_parts[:authority] = base_parts[:authority]
66
+ end
67
+ result_parts[:scheme] = base_parts[:scheme]
68
+ reconstruct_uri(result_parts)
69
+ end
70
+
71
+ def parse_uri(uri_str)
72
+ parts = {}
73
+ if uri_str.to_s =~ %r{\A(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?\z}
74
+ parts[:scheme] = Regexp.last_match(2)
75
+ parts[:authority] = Regexp.last_match(4)
76
+ parts[:path] = Regexp.last_match(5)
77
+ parts[:query] = Regexp.last_match(7)
78
+ end
79
+ parts
80
+ end
81
+
82
+ def merge_paths(base_path, ref_path)
83
+ if base_path&.include?("/")
84
+ base_path.sub(%r{/[^/]*\z}, "/#{ref_path}")
85
+ else
86
+ ref_path
87
+ end
88
+ end
89
+
90
+ # RFC 3986 §5.2.4 dot-segment removal with C14N 1.1 modifications.
91
+ TERMINAL_DOT_SEGMENTS = %w[. ..].freeze.freeze
92
+ private_constant :TERMINAL_DOT_SEGMENTS
93
+
94
+ def remove_dot_segments(path)
95
+ input = path.to_s.dup
96
+ input = input.sub(%r{/\.\.\z}, "/../")
97
+ output = +""
98
+
99
+ until input.empty?
100
+ if input.start_with?("../")
101
+ input = input[3..]
102
+ elsif input.start_with?("./")
103
+ input = input[2..]
104
+ elsif input.start_with?("/./")
105
+ input = "/#{input[3..]}"
106
+ elsif input == "/."
107
+ input = "/"
108
+ elsif input.start_with?("/../")
109
+ input = "/#{input[4..]}"
110
+ output = output.sub(%r{/[^/]*\z}, "")
111
+ elsif input == "/.."
112
+ input = "/"
113
+ output = output.sub(%r{/[^/]*\z}, "")
114
+ elsif TERMINAL_DOT_SEGMENTS.include?(input)
115
+ input = ""
116
+ else
117
+ seg_match = input.start_with?("/") ? input.match(%r{\A(/[^/]*)}) : input.match(%r{\A([^/]*)})
118
+ seg = seg_match[1]
119
+ input = input[seg.length..]
120
+ output << seg
121
+ end
122
+ end
123
+
124
+ output.squeeze("/").then do |out|
125
+ out << "/" if out.end_with?("/..")
126
+ out
127
+ end
128
+ end
129
+
130
+ def reconstruct_uri(parts)
131
+ result = +""
132
+ result << "#{parts[:scheme]}:" if parts[:scheme]
133
+ result << "//#{parts[:authority]}" if parts[:authority]
134
+ result << parts[:path].to_s if parts[:path]
135
+ result << "?#{parts[:query]}" if parts[:query]
136
+ result
137
+ end
138
+ end
139
+ end
140
+ end
data/lib/moxml/c14n.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Canonicalization engine. Core moxml feature — sibling to
5
+ # Moxml::XPath, Moxml::Builder, Moxml::SAX.
6
+ #
7
+ # Inclusive C14N (1.0 and 1.1) is ported from canon (lutaml/canon):
8
+ # mature, tested, full-featured (xml:base fixup, inheritable xml:*
9
+ # attribute resolution, node-set subset canonicalization).
10
+ #
11
+ # Exclusive C14N (1.0) is a moxml-native implementation; canon does
12
+ # not implement exclusive. It is required by XML signature to keep
13
+ # signatures valid when subdocuments are moved between contexts.
14
+ module C14n
15
+ autoload :Node, "moxml/c14n/node"
16
+ autoload :Nodes, "moxml/c14n/nodes"
17
+ autoload :DataModel, "moxml/c14n/data_model"
18
+ autoload :Processor, "moxml/c14n/processor"
19
+ autoload :CharacterEncoder, "moxml/c14n/character_encoder"
20
+ autoload :NamespaceHandler, "moxml/c14n/namespace_handler"
21
+ autoload :AttributeHandler, "moxml/c14n/attribute_handler"
22
+ autoload :XmlBaseHandler, "moxml/c14n/xml_base_handler"
23
+
24
+ # Engine classes used directly by signature algorithms.
25
+ autoload :Writer, "moxml/c14n/writer"
26
+ autoload :NamespaceContext, "moxml/c14n/namespace_context"
27
+ autoload :Exclusive, "moxml/c14n/exclusive"
28
+ autoload :Inclusive10, "moxml/c14n/inclusive_10"
29
+ autoload :Inclusive11, "moxml/c14n/inclusive_11"
30
+
31
+ XMLNS_URI = "http://www.w3.org/2000/xmlns/"
32
+ XML_URI = "http://www.w3.org/XML/1998/namespace"
33
+
34
+ # Canonicalize using the named algorithm.
35
+ #
36
+ # algorithm is one of:
37
+ # :inclusive10 Canonical XML 1.0 (default; W3C REC-xml-c14n-20010315)
38
+ # :inclusive11 Canonical XML 1.1 (W3C REC-xml-c14n11-20080502)
39
+ # :exclusive10 Exclusive C14N 1.0 (W3C REC-xml-exc-c14n-20020718)
40
+ def self.canonicalize(node_or_xml, with_comments: false,
41
+ algorithm: :inclusive10, inclusive_namespaces: [])
42
+ engine_for(algorithm).canonicalize(
43
+ node_or_xml,
44
+ with_comments: with_comments,
45
+ inclusive_namespaces: inclusive_namespaces,
46
+ )
47
+ end
48
+
49
+ def self.canonicalize_inclusive10(node_or_xml, with_comments: false)
50
+ Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
51
+ end
52
+
53
+ def self.canonicalize_inclusive11(node_or_xml, with_comments: false)
54
+ Inclusive11.new.canonicalize(node_or_xml, with_comments: with_comments)
55
+ end
56
+
57
+ def self.canonicalize_exclusive(node_or_xml, with_comments: false,
58
+ inclusive_namespaces: [])
59
+ Exclusive.new.canonicalize(
60
+ node_or_xml,
61
+ with_comments: with_comments,
62
+ inclusive_namespaces: inclusive_namespaces,
63
+ )
64
+ end
65
+
66
+ # Compare two XML inputs by their canonical forms.
67
+ def self.equivalent?(left, right, with_comments: false,
68
+ algorithm: :inclusive10, inclusive_namespaces: [])
69
+ canonicalize(left, with_comments: with_comments, algorithm: algorithm,
70
+ inclusive_namespaces: inclusive_namespaces) ==
71
+ canonicalize(right, with_comments: with_comments, algorithm: algorithm,
72
+ inclusive_namespaces: inclusive_namespaces)
73
+ end
74
+
75
+ def self.escape_text(text)
76
+ CharacterEncoder.new.encode_text(text)
77
+ end
78
+
79
+ def self.escape_attribute(value)
80
+ CharacterEncoder.new.encode_attribute(value)
81
+ end
82
+
83
+ def self.engine_for(algorithm)
84
+ case algorithm
85
+ when :inclusive10 then Inclusive10.new
86
+ when :inclusive11 then Inclusive11.new
87
+ when :exclusive10 then Exclusive.new
88
+ else
89
+ raise ArgumentError,
90
+ "unknown C14N algorithm #{algorithm.inspect}; expected one of " \
91
+ ":inclusive10, :inclusive11, :exclusive10"
92
+ end
93
+ end
94
+ private_class_method :engine_for
95
+ end
96
+ end