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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/README.adoc +64 -0
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +2 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- metadata +111 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Builds a C14N data model from a Moxml::Node tree (or XML string).
|
|
6
|
+
#
|
|
7
|
+
# The data model exists because canonicalization needs:
|
|
8
|
+
# - sorted namespace and attribute axes (spec §2.3, §2.4)
|
|
9
|
+
# - in_node_set flags for subset canonicalization (spec §3)
|
|
10
|
+
# - xml:* inheritable attribute resolution
|
|
11
|
+
#
|
|
12
|
+
# Ported from canon (lutaml/canon) — adapted to build directly from
|
|
13
|
+
# Moxml::Node rather than via a separate Nokogiri pass. Matches
|
|
14
|
+
# canon's document-level node iteration (PIs and comments outside
|
|
15
|
+
# the document root element).
|
|
16
|
+
class DataModel
|
|
17
|
+
def self.from_xml(xml_string)
|
|
18
|
+
from_node(::Moxml.parse(xml_string))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_node(moxml_node)
|
|
22
|
+
return build_from_document(moxml_node) if moxml_node.is_a?(::Moxml::Document)
|
|
23
|
+
|
|
24
|
+
root = Nodes::RootNode.new
|
|
25
|
+
root.add_child(build_node(moxml_node))
|
|
26
|
+
root
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build from a Moxml::Document. Matches canon's Nokogiri path:
|
|
30
|
+
# the root element is added first, then all other document-level
|
|
31
|
+
# children (PIs, comments) in document order.
|
|
32
|
+
def self.build_from_document(document)
|
|
33
|
+
root = Nodes::RootNode.new
|
|
34
|
+
|
|
35
|
+
if document.root
|
|
36
|
+
root.add_child(build_element_node(document.root))
|
|
37
|
+
# Iterate ALL document children — not just the root element.
|
|
38
|
+
# This captures PIs and comments that appear outside the
|
|
39
|
+
# document element, which are part of the canonical form.
|
|
40
|
+
document.children.each do |child|
|
|
41
|
+
next if child.equal?(document.root)
|
|
42
|
+
next if child.is_a?(::Moxml::Element)
|
|
43
|
+
|
|
44
|
+
built = build_node(child)
|
|
45
|
+
root.add_child(built) if built
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
root
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.build_node(moxml_node)
|
|
53
|
+
case moxml_node
|
|
54
|
+
when ::Moxml::Element then build_element_node(moxml_node)
|
|
55
|
+
when ::Moxml::Text then build_text_node(moxml_node)
|
|
56
|
+
when ::Moxml::Comment then build_comment_node(moxml_node)
|
|
57
|
+
when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.build_element_node(moxml_element)
|
|
62
|
+
ns = moxml_element.namespace
|
|
63
|
+
element = Nodes::ElementNode.new(
|
|
64
|
+
name: moxml_element.name,
|
|
65
|
+
namespace_uri: ns&.uri,
|
|
66
|
+
prefix: ns&.prefix,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
build_namespace_nodes(moxml_element, element)
|
|
70
|
+
build_attribute_nodes(moxml_element, element)
|
|
71
|
+
|
|
72
|
+
moxml_element.children.each do |child|
|
|
73
|
+
built = build_node(child)
|
|
74
|
+
element.add_child(built) if built
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
element
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.build_namespace_nodes(moxml_element, element)
|
|
81
|
+
moxml_element.in_scope_namespaces.each do |ns|
|
|
82
|
+
element.add_namespace(
|
|
83
|
+
Nodes::NamespaceNode.new(prefix: ns.prefix || "", uri: ns.uri),
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return if element.namespace_nodes.any? { |n| n.prefix == "xml" }
|
|
88
|
+
|
|
89
|
+
element.add_namespace(
|
|
90
|
+
Nodes::NamespaceNode.new(prefix: "xml", uri: XML_URI),
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.build_attribute_nodes(moxml_element, element)
|
|
95
|
+
moxml_element.attributes.each do |attr|
|
|
96
|
+
ns = attr.namespace
|
|
97
|
+
element.add_attribute(
|
|
98
|
+
Nodes::AttributeNode.new(
|
|
99
|
+
name: attr.name,
|
|
100
|
+
value: attr.value,
|
|
101
|
+
namespace_uri: ns&.uri,
|
|
102
|
+
prefix: ns&.prefix,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.build_text_node(moxml_text)
|
|
109
|
+
Nodes::TextNode.new(value: moxml_text.content)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.build_comment_node(moxml_comment)
|
|
113
|
+
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.build_pi_node(moxml_pi)
|
|
117
|
+
Nodes::ProcessingInstructionNode.new(
|
|
118
|
+
target: moxml_pi.target || moxml_pi.name,
|
|
119
|
+
data: moxml_pi.content || "",
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.mark_all(node, value)
|
|
124
|
+
node.in_node_set = value
|
|
125
|
+
node.children.each { |child| mark_all(child, value) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.mark_subset(root_node, matched)
|
|
129
|
+
matched.each { |node| mark_node_and_descendants(node) }
|
|
130
|
+
root_node.in_node_set = true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.mark_node_and_descendants(node)
|
|
134
|
+
node.in_node_set = true
|
|
135
|
+
node.children.each { |child| mark_node_and_descendants(child) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private_class_method :build_from_document, :build_node,
|
|
139
|
+
:build_element_node, :build_namespace_nodes,
|
|
140
|
+
:build_attribute_nodes, :build_text_node,
|
|
141
|
+
:build_comment_node, :build_pi_node,
|
|
142
|
+
:mark_node_and_descendants
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Exclusive XML Canonicalization 1.0 (https://www.w3.org/TR/xml-exc-c14n/)
|
|
6
|
+
#
|
|
7
|
+
# Renders only the namespaces visibly utilized by each element's
|
|
8
|
+
# qualified name and attributes (plus any in the inclusive prefix
|
|
9
|
+
# list), as opposed to inclusive C14N which renders every in-scope
|
|
10
|
+
# namespace on every element.
|
|
11
|
+
#
|
|
12
|
+
# Output is UTF-8 octets with no BOM.
|
|
13
|
+
class Exclusive
|
|
14
|
+
# `node`: a Moxml::Node — element, document, text, comment, or PI.
|
|
15
|
+
# `with_comments:`: include comment nodes in output.
|
|
16
|
+
# `inclusive_namespaces`: list of prefixes to always include even if
|
|
17
|
+
# not visibly utilized (InclusiveNamespacesPrefixList parameter).
|
|
18
|
+
def canonicalize(node, with_comments: false, inclusive_namespaces: [])
|
|
19
|
+
writer = Writer.new
|
|
20
|
+
context = NamespaceContext.new
|
|
21
|
+
render(node, writer, context, with_comments, inclusive_namespaces)
|
|
22
|
+
writer.output
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def render(node, writer, context, with_comments, inclusive)
|
|
28
|
+
case node_type(node)
|
|
29
|
+
when :document
|
|
30
|
+
node.children.each { |c| render(c, writer, context, with_comments, inclusive) }
|
|
31
|
+
when :element
|
|
32
|
+
render_element(node, writer, context, with_comments, inclusive)
|
|
33
|
+
when :text, :cdata
|
|
34
|
+
writer.text(node.content)
|
|
35
|
+
when :comment
|
|
36
|
+
writer.comment(node.content) if with_comments
|
|
37
|
+
when :processing_instruction
|
|
38
|
+
target, content = pi_target_and_content(node)
|
|
39
|
+
writer.processing_instruction(target, content)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_element(element, writer, context, with_comments, inclusive)
|
|
44
|
+
declared = bindings_declared_on(element)
|
|
45
|
+
in_scope = bindings_in_scope_on(element)
|
|
46
|
+
context.push(in_scope)
|
|
47
|
+
|
|
48
|
+
prefix = element_namespace_prefix(element)
|
|
49
|
+
local = element_name(element)
|
|
50
|
+
|
|
51
|
+
visible_prefixes = visibly_used_prefixes(element, prefix)
|
|
52
|
+
inclusive.each { |p| visible_prefixes.add(p) }
|
|
53
|
+
visible_prefixes.add("xml") if visibly_uses_xml(element)
|
|
54
|
+
|
|
55
|
+
namespaces_to_render = visible_prefixes.map do |p|
|
|
56
|
+
[p, context.uri_for(p)]
|
|
57
|
+
end.reject { |(_, uri)| uri.nil? }
|
|
58
|
+
|
|
59
|
+
writer.open_rendered_frame(element)
|
|
60
|
+
writer.open_tag(prefix, local)
|
|
61
|
+
render_namespaces(writer, element, namespaces_to_render)
|
|
62
|
+
render_attributes(writer, element)
|
|
63
|
+
writer.close_tag_open
|
|
64
|
+
|
|
65
|
+
# Switch context from in-scope (apex lookup) to declared-only so
|
|
66
|
+
# children build their own in-scope view by pushing their declared
|
|
67
|
+
# set on top of the parent's declared set.
|
|
68
|
+
context.pop
|
|
69
|
+
context.push(declared)
|
|
70
|
+
|
|
71
|
+
element.children.each { |c| render(c, writer, context, with_comments, inclusive) }
|
|
72
|
+
|
|
73
|
+
writer.close_tag(prefix, local)
|
|
74
|
+
writer.close_rendered_frame
|
|
75
|
+
context.pop
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_namespaces(writer, element, namespaces)
|
|
79
|
+
already_rendered = writer.rendered_namespaces_for(element)
|
|
80
|
+
sorted = namespaces
|
|
81
|
+
.reject { |(prefix, _)| already_rendered.include?(prefix) }
|
|
82
|
+
.sort_by { |(prefix, _)| prefix.to_s }
|
|
83
|
+
sorted.each do |(prefix, uri)|
|
|
84
|
+
writer.namespace(prefix, uri)
|
|
85
|
+
writer.mark_rendered(element, prefix, uri)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_attributes(writer, element)
|
|
90
|
+
attrs = collect_attributes(element)
|
|
91
|
+
sorted = attrs.sort_by { |a| [a[:ns_uri] || "", a[:local]] }
|
|
92
|
+
sorted.each { |a| writer.attribute(a[:expanded], a[:value]) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def collect_attributes(element)
|
|
96
|
+
attrs = []
|
|
97
|
+
element.attributes.each do |attr|
|
|
98
|
+
ns = attr.namespace
|
|
99
|
+
attrs << {
|
|
100
|
+
ns_uri: ns&.uri,
|
|
101
|
+
ns_prefix: ns&.prefix,
|
|
102
|
+
local: attr.name,
|
|
103
|
+
expanded: attr_expanded_name(attr),
|
|
104
|
+
value: attr.value,
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
attrs
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def attr_expanded_name(attr)
|
|
111
|
+
ns = attr.namespace
|
|
112
|
+
prefix = ns&.prefix
|
|
113
|
+
if prefix && !prefix.empty?
|
|
114
|
+
"#{prefix}:#{attr.name}"
|
|
115
|
+
else
|
|
116
|
+
attr.name
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def visibly_used_prefixes(element, element_prefix)
|
|
121
|
+
set = Set.new
|
|
122
|
+
# The element's qualified name visibly uses its own namespace.
|
|
123
|
+
# Default namespace (prefix == "" or nil) is included so it gets
|
|
124
|
+
# rendered on the apex element when the apex uses default ns.
|
|
125
|
+
if element.namespace_uri
|
|
126
|
+
set.add(element_prefix || "")
|
|
127
|
+
end
|
|
128
|
+
element.attributes.each do |attr|
|
|
129
|
+
ns = attr.namespace
|
|
130
|
+
prefix = ns&.prefix
|
|
131
|
+
set.add(prefix || "") if ns&.uri
|
|
132
|
+
end
|
|
133
|
+
set
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def visibly_uses_xml(element)
|
|
137
|
+
element.attributes.any? { |a| a.namespace&.prefix == "xml" }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def bindings_declared_on(element)
|
|
141
|
+
bindings = {}
|
|
142
|
+
element.namespaces.each do |ns|
|
|
143
|
+
prefix = ns.prefix
|
|
144
|
+
prefix = "" if prefix.nil? || prefix == "xmlns"
|
|
145
|
+
bindings[prefix] = ns.uri
|
|
146
|
+
end
|
|
147
|
+
bindings
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# In-scope namespaces (declared + inherited from ancestors).
|
|
151
|
+
# Used at the apex to find URIs for visibly-used prefixes whose
|
|
152
|
+
# declarations live on ancestors.
|
|
153
|
+
def bindings_in_scope_on(element)
|
|
154
|
+
bindings = {}
|
|
155
|
+
element.in_scope_namespaces.each do |ns|
|
|
156
|
+
prefix = ns.prefix
|
|
157
|
+
prefix = "" if prefix.nil? || prefix == "xmlns"
|
|
158
|
+
bindings[prefix] = ns.uri
|
|
159
|
+
end
|
|
160
|
+
bindings
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def element_namespace_prefix(element)
|
|
164
|
+
ns = element.namespace
|
|
165
|
+
ns&.prefix
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def element_name(element)
|
|
169
|
+
element.name
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def pi_target_and_content(node)
|
|
173
|
+
[node.name, node.text]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def node_type(node)
|
|
177
|
+
return :document if node.is_a?(::Moxml::Document)
|
|
178
|
+
return :element if node.is_a?(::Moxml::Element)
|
|
179
|
+
return :text if node.is_a?(::Moxml::Text)
|
|
180
|
+
return :cdata if node.is_a?(::Moxml::Cdata)
|
|
181
|
+
return :comment if node.is_a?(::Moxml::Comment)
|
|
182
|
+
return :processing_instruction if node.is_a?(::Moxml::ProcessingInstruction)
|
|
183
|
+
|
|
184
|
+
:unknown
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Inclusive Canonical XML 1.0 (https://www.w3.org/TR/xml-c14n/)
|
|
6
|
+
#
|
|
7
|
+
# Unlike exclusive C14N, inclusive C14N "attracts" ancestor context:
|
|
8
|
+
# at the apex element, ALL in-scope namespaces are rendered, including
|
|
9
|
+
# those inherited from ancestors outside the canonicalization subset.
|
|
10
|
+
#
|
|
11
|
+
# xml:* inheritable attributes (xml:lang, xml:space) are also inherited
|
|
12
|
+
# from the nearest ancestor in which they are declared.
|
|
13
|
+
#
|
|
14
|
+
# Implementation: delegates to the canon-derived Processor pipeline
|
|
15
|
+
# (DataModel + NamespaceHandler + AttributeHandler + XmlBaseHandler).
|
|
16
|
+
class Inclusive10
|
|
17
|
+
# rubocop:disable Lint/UnusedMethodArgument -- signature must match Exclusive
|
|
18
|
+
def canonicalize(node_or_xml, with_comments: false, inclusive_namespaces: [])
|
|
19
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
20
|
+
root = coerce_to_data_model(node_or_xml)
|
|
21
|
+
Processor.new(with_comments: with_comments).process(root)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def coerce_to_data_model(node_or_xml)
|
|
27
|
+
return node_or_xml if node_or_xml.is_a?(Nodes::RootNode)
|
|
28
|
+
|
|
29
|
+
case node_or_xml
|
|
30
|
+
when ::Moxml::Document, ::Moxml::Node
|
|
31
|
+
DataModel.from_node(node_or_xml)
|
|
32
|
+
when String
|
|
33
|
+
DataModel.from_xml(node_or_xml)
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"Inclusive10#canonicalize expects a Moxml::Node, " \
|
|
37
|
+
"Moxml::Document, or XML String; got #{node_or_xml.class}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Canonical XML 1.1 (https://www.w3.org/TR/xml-c14n11/).
|
|
6
|
+
#
|
|
7
|
+
# In this implementation, Inclusive11 is a thin alias of Inclusive10.
|
|
8
|
+
# Both delegate to the canon-derived Processor. The W3C 1.1 additions
|
|
9
|
+
# (notations, entity references in DTD internal subset, XML 1.1 line
|
|
10
|
+
# ending) are rarely encountered in modern XML signature practice.
|
|
11
|
+
class Inclusive11
|
|
12
|
+
def canonicalize(node_or_xml, with_comments: false, inclusive_namespaces: [])
|
|
13
|
+
Inclusive10.new.canonicalize(
|
|
14
|
+
node_or_xml,
|
|
15
|
+
with_comments: with_comments,
|
|
16
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Tracks in-scope namespace URIs along the ancestor chain of the
|
|
6
|
+
# element currently being canonicalized. The canonicalizer pushes
|
|
7
|
+
# when entering an element and pops when leaving.
|
|
8
|
+
#
|
|
9
|
+
# `xml` prefix is always bound to the XML namespace per XML 1.0 spec.
|
|
10
|
+
class NamespaceContext
|
|
11
|
+
DEFAULT_XML_BINDING = { "xml" => C14n::XML_URI }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :bindings
|
|
14
|
+
|
|
15
|
+
def initialize(initial = {})
|
|
16
|
+
@stack = []
|
|
17
|
+
@bindings = DEFAULT_XML_BINDING.dup
|
|
18
|
+
merge(initial)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def push(bindings_to_apply)
|
|
22
|
+
applied = {}
|
|
23
|
+
bindings_to_apply.each do |prefix, uri|
|
|
24
|
+
prev = @bindings[prefix]
|
|
25
|
+
applied[prefix] = prev
|
|
26
|
+
@bindings[prefix] = uri
|
|
27
|
+
end
|
|
28
|
+
@stack.push(applied)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pop
|
|
33
|
+
applied = @stack.pop
|
|
34
|
+
applied.each do |prefix, prev|
|
|
35
|
+
if prev.nil?
|
|
36
|
+
@bindings.delete(prefix)
|
|
37
|
+
@bindings[prefix] = C14n::XML_URI if prefix == "xml"
|
|
38
|
+
else
|
|
39
|
+
@bindings[prefix] = prev
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def uri_for(prefix)
|
|
46
|
+
@bindings[prefix]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def binding_for(prefix)
|
|
50
|
+
@bindings[prefix]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def include?(prefix)
|
|
54
|
+
@bindings.key?(prefix)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
@bindings.dup
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def merge(hash)
|
|
64
|
+
hash.each { |prefix, uri| @bindings[prefix] = uri }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Namespace axis handler for inclusive C14N.
|
|
6
|
+
# Implements W3C C14N 1.0 §2.3 / 1.1 §2.3 namespace rendering rules
|
|
7
|
+
# for the document subset (full and partial).
|
|
8
|
+
class NamespaceHandler
|
|
9
|
+
attr_reader :encoder
|
|
10
|
+
|
|
11
|
+
def initialize(encoder)
|
|
12
|
+
@encoder = encoder
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def process_namespaces(element, output, parent_element = nil)
|
|
16
|
+
return unless element.in_node_set?
|
|
17
|
+
|
|
18
|
+
namespaces = element.sorted_namespace_nodes.select(&:in_node_set?)
|
|
19
|
+
|
|
20
|
+
if should_emit_empty_default_namespace?(element, namespaces, parent_element)
|
|
21
|
+
output << ' xmlns=""'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
namespaces.each do |ns|
|
|
25
|
+
next if should_skip_namespace?(ns, parent_element)
|
|
26
|
+
|
|
27
|
+
output << " "
|
|
28
|
+
output << (ns.default_namespace? ? "xmlns" : "xmlns:#{ns.prefix}")
|
|
29
|
+
output << '="'
|
|
30
|
+
output << encoder.encode_attribute(ns.uri)
|
|
31
|
+
output << '"'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Emit xmlns="" if the element's nearest in-set ancestor had a non-empty
|
|
38
|
+
# default namespace and this element does not.
|
|
39
|
+
def should_emit_empty_default_namespace?(element, namespaces, parent_element)
|
|
40
|
+
return false unless element.in_node_set?
|
|
41
|
+
return false if namespaces.first&.default_namespace?
|
|
42
|
+
return false unless parent_element
|
|
43
|
+
|
|
44
|
+
parent_default_ns = parent_element.namespace_nodes.find do |ns|
|
|
45
|
+
ns.default_namespace? && ns.in_node_set?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
parent_default_ns && !parent_default_ns.uri.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def should_skip_namespace?(namespace, parent_element)
|
|
52
|
+
# The xml namespace is implicit, never rendered.
|
|
53
|
+
return true if namespace.xml_namespace?
|
|
54
|
+
# Skip namespaces already declared (with same URI) by an ancestor.
|
|
55
|
+
return true if namespace_declared_by_ancestor?(namespace, parent_element)
|
|
56
|
+
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def namespace_declared_by_ancestor?(namespace, parent_element)
|
|
61
|
+
return false unless parent_element
|
|
62
|
+
|
|
63
|
+
parent_ns = parent_element.namespace_nodes.find do |candidate|
|
|
64
|
+
candidate.prefix == namespace.prefix && candidate.in_node_set?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
parent_ns && parent_ns.uri == namespace.uri
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Base class for all C14N data-model nodes.
|
|
6
|
+
# Ported from canon ( lutaml/canon ) — keeps the mature node-set
|
|
7
|
+
# semantics needed for subset canonicalization (spec §3).
|
|
8
|
+
class Node
|
|
9
|
+
attr_reader :parent, :children
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@parent = nil
|
|
13
|
+
@children = []
|
|
14
|
+
@in_node_set = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_child(child)
|
|
18
|
+
child.parent = self
|
|
19
|
+
@children << child
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def in_node_set?
|
|
23
|
+
@in_node_set
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def in_node_set=(value)
|
|
27
|
+
@in_node_set = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the text content of this node and all descendants.
|
|
31
|
+
# ElementNode concatenates children's text_content; other nodes
|
|
32
|
+
# (TextNode, CommentNode, etc.) return their value.
|
|
33
|
+
def text_content
|
|
34
|
+
children.map(&:text_content).join
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
attr_writer :parent
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
module Nodes
|
|
6
|
+
# Attribute node.
|
|
7
|
+
class AttributeNode < Node
|
|
8
|
+
attr_reader :name, :value, :namespace_uri, :prefix
|
|
9
|
+
|
|
10
|
+
def initialize(name:, value:, namespace_uri: nil, prefix: nil)
|
|
11
|
+
super()
|
|
12
|
+
@name = name
|
|
13
|
+
@value = value
|
|
14
|
+
@namespace_uri = namespace_uri
|
|
15
|
+
@prefix = prefix
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def node_type
|
|
19
|
+
:attribute
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def local_name
|
|
23
|
+
name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def qname
|
|
27
|
+
prefix.nil? || prefix.empty? ? name : "#{prefix}:#{name}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# xml:* attributes (lang, space, base, id).
|
|
31
|
+
def xml_attribute?
|
|
32
|
+
namespace_uri == Moxml::C14n::XML_URI
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# xml:lang and xml:space are inheritable per C14N 1.1 §2.4.
|
|
36
|
+
def simple_inheritable?
|
|
37
|
+
xml_attribute? && %w[lang space].include?(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def xml_id?
|
|
41
|
+
xml_attribute? && name == "id"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def xml_base?
|
|
45
|
+
xml_attribute? && name == "base"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
module Nodes
|
|
6
|
+
# Comment node.
|
|
7
|
+
class CommentNode < Node
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(value:)
|
|
11
|
+
super()
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
"comment"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def node_type
|
|
20
|
+
:comment
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def text_content
|
|
24
|
+
@value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|