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,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
|