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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +188 -0
- data/Rakefile +116 -4
- 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/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter.rb +5 -3
- 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/config.rb +1 -1
- 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/moxml.gemspec +1 -1
- 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/adapter/platform_spec.rb +64 -46
- 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/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -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
- data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- 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 = :
|
|
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
|