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,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
data/lib/moxml/config.rb CHANGED
@@ -7,7 +7,7 @@ module Moxml
7
7
  VALID_LINE_ENDINGS = [LINE_ENDING_LF, LINE_ENDING_CRLF].freeze
8
8
  VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
9
9
  DEFAULT_ADAPTER = :nokogiri
10
- OPAL_DEFAULT_ADAPTER = :rexml
10
+ OPAL_DEFAULT_ADAPTER = :oga
11
11
 
12
12
  # Entity loading modes:
13
13
  # - :required - Must load entities, raise error if unavailable (default)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # Base64 decode transform (W3C §6.6.2).
9
+ #
10
+ # Input: octets or nodeset. For nodeset, logically applies self::text(),
11
+ # sorts nodes by document order, concatenates string values, then decodes.
12
+ # Output: octets.
13
+ class Base64Transform < TransformBase
14
+ identifier "http://www.w3.org/2000/09/xmldsig#base64"
15
+
16
+ def self.input_type; :octets; end
17
+ def self.output_type; :octets; end
18
+
19
+ def transform(input)
20
+ text = input.is_a?(Array) ? input.map { |n| text_of(n) }.join : input.to_s
21
+ stripped = text.gsub(/\s+/, "")
22
+ Base64.strict_decode64(stripped)
23
+ rescue ArgumentError => e
24
+ raise TransformError.new(
25
+ "base64 decode failed: #{e.message}",
26
+ algorithm: self.class.identifier_uri,
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def text_of(node)
33
+ return node.text if node.is_a?(::Moxml::Node)
34
+
35
+ node.to_s
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # Base class for canonicalization algorithms.
9
+ #
10
+ # Subclasses declare `identifier "http://..."` and implement #engine
11
+ # (returning a Moxml::C14n::* walker). Canonicalizers operate on a
12
+ # Moxml::Node subtree and return UTF-8 octets.
13
+ #
14
+ # Canonicalization algorithms can also be used as transforms per
15
+ # spec §6.6.1. The base class provides the #transform method that
16
+ # adapts the canonicalize interface to the transform pipeline.
17
+ class CanonicalizationBase
18
+ class << self
19
+ # Canonicalization presents the transform interface (spec §6.6.1).
20
+ # Input: octet stream or node-set. Output: octet stream.
21
+ def input_type; :nodeset; end
22
+
23
+ def output_type; :octets; end
24
+
25
+ def identifier(uri)
26
+ Algorithms.register(:canonicalization, uri, self)
27
+ end
28
+
29
+ def for_uri(uri, **opts)
30
+ new(identifier_uri: uri, **opts)
31
+ end
32
+ end
33
+
34
+ attr_reader :identifier_uri, :with_comments, :inclusive_namespaces,
35
+ :context
36
+
37
+ # `context:` is required when this algorithm is used as a transform
38
+ # so that octet-stream input can be parsed with the same adapter
39
+ # the caller used for the rest of the document.
40
+ def initialize(identifier_uri: nil, with_comments: nil,
41
+ inclusive_namespaces: [], context: nil, **_unused)
42
+ @identifier_uri = identifier_uri
43
+ @with_comments = with_comments.nil? ? uri_has_comments?(identifier_uri) : with_comments
44
+ @inclusive_namespaces = inclusive_namespaces || []
45
+ @context = context
46
+ end
47
+
48
+ def canonicalize(node)
49
+ engine.canonicalize(
50
+ node,
51
+ with_comments: @with_comments,
52
+ inclusive_namespaces: @inclusive_namespaces,
53
+ )
54
+ end
55
+
56
+ # Adapt to the transform interface (spec §6.6.1). Octet-stream
57
+ # input is parsed using the same Moxml::Context the caller used;
58
+ # this preserves the byte-exact invariant across adapters.
59
+ def transform(input)
60
+ return canonicalize(input) if input.is_a?(::Moxml::Node)
61
+
62
+ if context.nil?
63
+ raise TransformError,
64
+ "canonicalization transform requires a context to parse " \
65
+ "octet-stream input; none was provided"
66
+ end
67
+
68
+ parsed = context.parse(input.to_s)
69
+ canonicalize(parsed.root)
70
+ end
71
+
72
+ private
73
+
74
+ def uri_has_comments?(uri)
75
+ return false if uri.nil?
76
+
77
+ uri.end_with?("#WithComments")
78
+ end
79
+
80
+ def engine
81
+ raise NotImplementedError,
82
+ "#{self.class} must implement #engine"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Moxml
7
+ module Signature
8
+ module Algorithms
9
+ # Base class for message digest algorithms (SHA family).
10
+ #
11
+ # Subclasses MUST declare:
12
+ # - `identifier "http://..."` to register
13
+ # - either `digest_bits N` and `openssl_digest_name "SHA256"` etc.,
14
+ # OR override `#compute_digest`.
15
+ class DigestBase
16
+ class << self
17
+ attr_reader :identifier_uri, :digest_size_bits
18
+
19
+ def identifier(uri)
20
+ @identifier_uri = uri
21
+ Algorithms.register(:digest, uri, self)
22
+ end
23
+
24
+ def digest_bits(bits)
25
+ @digest_size_bits = bits
26
+ end
27
+
28
+ def openssl_digest_name(name)
29
+ define_method(:compute_digest) do |data|
30
+ OpenSSL::Digest.digest(name, data)
31
+ end
32
+ end
33
+ end
34
+
35
+ def digest(data)
36
+ compute_digest(data.to_s)
37
+ end
38
+
39
+ def digest_base64(data)
40
+ Base64.strict_encode64(digest(data))
41
+ end
42
+
43
+ def compute_digest(_data)
44
+ raise NotImplementedError,
45
+ "#{self.class} must implement #compute_digest " \
46
+ "or declare openssl_digest_name"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end