moxml 0.1.25 → 0.2.0

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/.github/workflows/windows.yml +62 -0
  4. data/Gemfile +3 -1
  5. data/README.adoc +133 -6
  6. data/docs/signature/algorithms.md +108 -0
  7. data/docs/signature/architecture.md +148 -0
  8. data/docs/signature/c14n.md +110 -0
  9. data/docs/signature/examples.md +109 -0
  10. data/docs/signature/flows.md +132 -0
  11. data/docs/signature/quick-reference.md +115 -0
  12. data/docs/signature/security.md +143 -0
  13. data/examples/signature/auto_key_extraction.rb +24 -0
  14. data/examples/signature/enveloped_rsa.rb +45 -0
  15. data/examples/signature/hmac.rb +33 -0
  16. data/lib/compat/opal/moxml_boot.rb +5 -0
  17. data/lib/moxml/adapter/base.rb +53 -49
  18. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  19. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  20. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  21. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  22. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  23. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  24. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  25. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  26. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  27. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  28. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  29. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  30. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  31. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  32. data/lib/moxml/adapter/customized_oga.rb +2 -0
  33. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  34. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  35. data/lib/moxml/adapter/headed_ox.rb +9 -128
  36. data/lib/moxml/adapter/leptris.rb +965 -0
  37. data/lib/moxml/adapter/libxml.rb +131 -99
  38. data/lib/moxml/adapter/nokogiri.rb +29 -8
  39. data/lib/moxml/adapter/oga.rb +91 -54
  40. data/lib/moxml/adapter/ox.rb +93 -137
  41. data/lib/moxml/adapter/rexml.rb +61 -50
  42. data/lib/moxml/adapter.rb +2 -1
  43. data/lib/moxml/attribute.rb +8 -4
  44. data/lib/moxml/attribute_resolver.rb +189 -0
  45. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  46. data/lib/moxml/c14n/character_encoder.rb +29 -0
  47. data/lib/moxml/c14n/data_model.rb +145 -0
  48. data/lib/moxml/c14n/exclusive.rb +188 -0
  49. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  50. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  51. data/lib/moxml/c14n/namespace_context.rb +68 -0
  52. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  53. data/lib/moxml/c14n/node.rb +42 -0
  54. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  55. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  56. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  57. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  58. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  59. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  60. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  61. data/lib/moxml/c14n/nodes.rb +17 -0
  62. data/lib/moxml/c14n/processor.rb +118 -0
  63. data/lib/moxml/c14n/writer.rb +121 -0
  64. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  65. data/lib/moxml/c14n.rb +96 -0
  66. data/lib/moxml/config.rb +29 -3
  67. data/lib/moxml/context.rb +12 -0
  68. data/lib/moxml/element.rb +29 -12
  69. data/lib/moxml/entity/reference.rb +28 -0
  70. data/lib/moxml/entity.rb +125 -0
  71. data/lib/moxml/node.rb +90 -1
  72. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  73. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  74. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  75. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  76. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  77. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  78. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  79. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  80. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  81. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  82. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  83. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  84. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  85. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  86. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  87. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  88. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  89. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  90. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  91. data/lib/moxml/signature/algorithms.rb +113 -0
  92. data/lib/moxml/signature/errors.rb +99 -0
  93. data/lib/moxml/signature/key_extractor.rb +172 -0
  94. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  95. data/lib/moxml/signature/model/digest_method.rb +15 -0
  96. data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
  97. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  98. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  99. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  100. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  101. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  102. data/lib/moxml/signature/model/key_info.rb +21 -0
  103. data/lib/moxml/signature/model/key_value.rb +20 -0
  104. data/lib/moxml/signature/model/object_element.rb +18 -0
  105. data/lib/moxml/signature/model/reference.rb +22 -0
  106. data/lib/moxml/signature/model/signature.rb +21 -0
  107. data/lib/moxml/signature/model/signature_value.rb +16 -0
  108. data/lib/moxml/signature/model/signed_info.rb +20 -0
  109. data/lib/moxml/signature/model/transform.rb +16 -0
  110. data/lib/moxml/signature/model/transforms.rb +36 -0
  111. data/lib/moxml/signature/model.rb +35 -0
  112. data/lib/moxml/signature/parser.rb +270 -0
  113. data/lib/moxml/signature/reference_resolver.rb +79 -0
  114. data/lib/moxml/signature/reference_result.rb +22 -0
  115. data/lib/moxml/signature/serializer.rb +163 -0
  116. data/lib/moxml/signature/signer.rb +73 -0
  117. data/lib/moxml/signature/single_verification_result.rb +31 -0
  118. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  119. data/lib/moxml/signature/verification_result.rb +27 -0
  120. data/lib/moxml/signature/verifier.rb +129 -0
  121. data/lib/moxml/signature.rb +94 -0
  122. data/lib/moxml/version.rb +1 -1
  123. data/lib/moxml/xml_emitter.rb +71 -0
  124. data/lib/moxml/xpath/compiler.rb +60 -7
  125. data/lib/moxml/xpath/parser.rb +22 -15
  126. data/lib/moxml.rb +5 -0
  127. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  128. data/reference-docs/w3c-xmldsig-core.md +400 -0
  129. data/spec/examples/readme_examples_spec.rb +0 -4
  130. data/spec/examples/xpath_examples_spec.rb +0 -11
  131. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  132. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  133. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  134. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  135. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  136. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  137. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  138. data/spec/integration/all_adapters_spec.rb +17 -0
  139. data/spec/integration/sax_parity_spec.rb +58 -0
  140. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  141. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  142. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  143. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  144. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  145. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  146. data/spec/moxml/adapter/ox_spec.rb +20 -6
  147. data/spec/moxml/adapter/platform_spec.rb +15 -2
  148. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  149. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  150. data/spec/moxml/c14n/api_spec.rb +117 -0
  151. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  152. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  153. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  154. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  155. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  156. data/spec/moxml/doctype_spec.rb +1 -1
  157. data/spec/moxml/entity_spec.rb +76 -0
  158. data/spec/moxml/node_spec.rb +104 -0
  159. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  160. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  161. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  162. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  163. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  164. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  165. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  166. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  167. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  168. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  169. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  170. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  171. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  172. data/spec/moxml/signature/model_spec.rb +54 -0
  173. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  174. data/spec/moxml/xml_emitter_spec.rb +64 -0
  175. data/spec/moxml/xpath/axes_spec.rb +72 -0
  176. data/spec/moxml/xpath/parser_spec.rb +6 -0
  177. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  178. data/spec/performance/benchmark_spec.rb +13 -6
  179. data/spec/performance/thread_safety_spec.rb +0 -4
  180. data/spec/spec_helper.rb +5 -1
  181. metadata +130 -2
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Shared transform-pipeline logic used by both Signer (signing flow,
6
+ # signature_element: nil) and Verifier (verification flow, with the
7
+ # containing Signature element so the Enveloped Signature transform
8
+ # can remove it from the digest input).
9
+ #
10
+ # Each Transform in the chain is looked up via Algorithms.lookup(:transform, uri),
11
+ # with a fallback to Algorithms.lookup(:canonicalization, uri) per
12
+ # spec §6.6.1 (any canonicalization algorithm can be used as a transform).
13
+ #
14
+ # Type coercion matches spec §4.4.3.2 reference processing model:
15
+ # - octets → nodeset: parse as XML
16
+ # - nodeset → octets: apply inclusive C14N 1.0
17
+ class TransformPipeline
18
+ attr_reader :context, :signature_element
19
+
20
+ def initialize(context:, signature_element: nil)
21
+ @context = context
22
+ @signature_element = signature_element
23
+ end
24
+
25
+ def apply(input, transforms_model)
26
+ current = input
27
+ current_type = type_of(current)
28
+ transforms = transforms_from(transforms_model)
29
+
30
+ transforms.each do |transform_model|
31
+ algo_class = lookup(transform_model.algorithm)
32
+ transform = algo_class.new(
33
+ parameters: transform_model.parameters,
34
+ context: context,
35
+ signature_element: signature_element,
36
+ )
37
+ current_type, current = coerce(current, current_type,
38
+ algo_class.input_type)
39
+ current = transform.transform(current)
40
+ current_type = algo_class.output_type
41
+ end
42
+
43
+ current
44
+ end
45
+
46
+ # Final step: convert the pipeline output (which may still be a node)
47
+ # to octets suitable for digesting. Uses inclusive C14N 1.0 as the
48
+ # default mapping per spec §4.4.3.2.
49
+ def to_octets(value, reference = nil)
50
+ return value if value.is_a?(String)
51
+
52
+ c14n_uri = canonicalization_from(reference) || DEFAULT_OCTET_C14N
53
+ klass = Algorithms.lookup(:canonicalization, c14n_uri)
54
+ klass.new(identifier_uri: c14n_uri).canonicalize(value)
55
+ end
56
+
57
+ DEFAULT_OCTET_C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
58
+
59
+ private_constant :DEFAULT_OCTET_C14N
60
+
61
+ private
62
+
63
+ def transforms_from(transforms_model)
64
+ return [] unless transforms_model
65
+
66
+ transforms_model.transforms
67
+ end
68
+
69
+ def lookup(uri)
70
+ Algorithms.lookup(:transform, uri)
71
+ rescue UnknownAlgorithm
72
+ Algorithms.lookup(:canonicalization, uri)
73
+ end
74
+
75
+ def type_of(value)
76
+ case value
77
+ when ::Moxml::Node, Array then :nodeset
78
+ else :octets
79
+ end
80
+ end
81
+
82
+ def coerce(input, from, to)
83
+ return [to, input] if from == to
84
+
85
+ case [from, to]
86
+ when %i[octets nodeset]
87
+ parsed = context.parse(input.to_s)
88
+ [:nodeset, parsed.root]
89
+ when %i[nodeset octets]
90
+ octets = C14n::Inclusive10.new.canonicalize(input)
91
+ [:octets, octets]
92
+ else
93
+ raise TransformError, "cannot coerce #{from} to #{to}"
94
+ end
95
+ end
96
+
97
+ def canonicalization_from(reference)
98
+ return nil unless reference&.transforms
99
+
100
+ reference.transforms.transforms.reverse_each.find do |t|
101
+ Algorithms.registered?(:canonicalization, t.algorithm)
102
+ end&.algorithm
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Aggregated verification result for a document. A document may
6
+ # contain multiple Signature elements; this result wraps them all.
7
+ class VerificationResult
8
+ attr_reader :results
9
+
10
+ def initialize(results:)
11
+ @results = results
12
+ end
13
+
14
+ def valid?
15
+ results.all?(&:valid?)
16
+ end
17
+
18
+ def signature_count
19
+ results.size
20
+ end
21
+
22
+ def failing
23
+ results.reject(&:valid?)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ # Core Validation per spec §3.2, with the safe ordering prescribed by
6
+ # Best Practice 1: verify SignatureValue BEFORE running any reference
7
+ # transforms (which could be hostile).
8
+ class Verifier
9
+ attr_reader :context, :document, :key, :key_map
10
+
11
+ def initialize(context:, document:, key: nil, key_map: {}, **_options)
12
+ @context = context
13
+ @document = document
14
+ @key = key
15
+ @key_map = key_map || {}
16
+ end
17
+
18
+ def verify
19
+ Algorithms.load_builtins!
20
+ signatures = find_signatures
21
+
22
+ results = signatures.map { |sig_elem| verify_one(sig_elem) }
23
+ VerificationResult.new(results: results)
24
+ end
25
+
26
+ private
27
+
28
+ def find_signatures
29
+ document.xpath("//ds:Signature", "ds" => DSIG_NS)
30
+ .grep(::Moxml::Element)
31
+ end
32
+
33
+ def verify_one(signature_element)
34
+ signature = Parser.new(context: context).parse(signature_element)
35
+ signature_value_ok, error = verify_signature_value(signature, signature_element)
36
+ reference_results = if signature_value_ok
37
+ verify_references(signature, signature_element)
38
+ else
39
+ []
40
+ end
41
+
42
+ SingleVerificationResult.new(
43
+ signature_id: signature.id,
44
+ signature_valid: signature_value_ok,
45
+ references: reference_results,
46
+ error: error,
47
+ )
48
+ end
49
+
50
+ # Returns [Boolean, ErrorOrNil] so the caller can attach the error
51
+ # to the result for debugging. The error is never raised — Best
52
+ # Practice 1 says authenticate first; surfacing why auth failed is
53
+ # an application concern, not a panic.
54
+ def verify_signature_value(signature, signature_element)
55
+ return [false, nil] if signature.signature_value.nil?
56
+ return [false, nil] if signature.signature_value.value.nil?
57
+
58
+ signed_info_elem = signature_element.at_xpath(
59
+ "./ds:SignedInfo", "ds" => DSIG_NS
60
+ )
61
+ return [false, nil] if signed_info_elem.nil?
62
+
63
+ c14n_uri = signature.signed_info.canonicalization_method&.algorithm
64
+ return [false, nil] if c14n_uri.nil?
65
+
66
+ canonical = canonicalize_signed_info(c14n_uri, signed_info_elem)
67
+ sm_uri = signature.signed_info.signature_method.algorithm
68
+ sm = Algorithms.lookup(:signature_method, sm_uri).new(
69
+ identifier_uri: sm_uri,
70
+ parameters: signature.signed_info.signature_method.parameters,
71
+ )
72
+ verification_key = resolve_key(signature)
73
+ return [false, nil] if verification_key.nil?
74
+
75
+ ok = sm.verify(canonical, verification_key, signature.signature_value.value)
76
+ [ok, nil]
77
+ rescue VerificationError, UnknownAlgorithm => e
78
+ [false, e]
79
+ end
80
+
81
+ # Canonicalize the original SignedInfo element as it appears in the
82
+ # document. Re-serializing the parsed model would change namespace
83
+ # prefixes (e.g. SignedInfo → ds:SignedInfo) and break byte-exact
84
+ # verification.
85
+ def canonicalize_signed_info(c14n_uri, signed_info_elem)
86
+ Algorithms.lookup(:canonicalization, c14n_uri)
87
+ .new(identifier_uri: c14n_uri)
88
+ .canonicalize(signed_info_elem)
89
+ end
90
+
91
+ def resolve_key(signature)
92
+ return key if key
93
+ return nil unless signature.key_info
94
+
95
+ KeyExtractor.new(key_map: key_map).extract(signature.key_info)
96
+ end
97
+
98
+ def verify_references(signature, signature_element)
99
+ signature.signed_info.references.map do |ref|
100
+ verify_reference(ref, signature_element)
101
+ end
102
+ end
103
+
104
+ def verify_reference(reference, signature_element)
105
+ resolver = ReferenceResolver.new(context: context, document: document)
106
+ input = resolver.resolve(reference.uri)
107
+ pipeline = TransformPipeline.new(
108
+ context: context,
109
+ signature_element: signature_element,
110
+ )
111
+ transformed = pipeline.apply(input, reference.transforms)
112
+ canonical = pipeline.to_octets(transformed, reference)
113
+
114
+ digest_algo = Algorithms.lookup(
115
+ :digest, reference.digest_method.algorithm
116
+ ).new
117
+ computed = digest_algo.digest_base64(canonical)
118
+ expected = (reference.digest_value || "").strip
119
+
120
+ ReferenceResult.new(
121
+ uri: reference.uri,
122
+ digest_method: reference.digest_method.algorithm,
123
+ expected: expected,
124
+ computed: computed,
125
+ )
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Moxml
7
+ # W3C XML Signature (xmldsig-core-1.1) processing for any moxml adapter.
8
+ #
9
+ # All XML operations flow through Moxml::Document / Moxml::Element; the
10
+ # signature module never touches the underlying adapter (Nokogiri, Oga,
11
+ # REXML, Ox, LibXML) directly.
12
+ module Signature
13
+ DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
14
+ DSIG11_NS = "http://www.w3.org/2009/xmldsig11#"
15
+ DSIG_MORE_NS = "http://www.w3.org/2001/04/xmldsig-more#"
16
+
17
+ autoload :Error, "moxml/signature/errors"
18
+ autoload :SignatureError, "moxml/signature/errors"
19
+ autoload :UnknownAlgorithm, "moxml/signature/errors"
20
+ autoload :DuplicateAlgorithm, "moxml/signature/errors"
21
+ autoload :SigningError, "moxml/signature/errors"
22
+ autoload :VerificationError, "moxml/signature/errors"
23
+ autoload :ReferenceDigestMismatch, "moxml/signature/errors"
24
+ autoload :SignatureValueMismatch, "moxml/signature/errors"
25
+ autoload :TransformError, "moxml/signature/errors"
26
+ autoload :CanonicalizationError, "moxml/signature/errors"
27
+ autoload :MalformedSignatureError, "moxml/signature/errors"
28
+ autoload :SignatureKeyError, "moxml/signature/errors"
29
+
30
+ autoload :Algorithms, "moxml/signature/algorithms"
31
+ autoload :C14n, "moxml/c14n"
32
+ autoload :Model, "moxml/signature/model"
33
+ autoload :Serializer, "moxml/signature/serializer"
34
+ autoload :Parser, "moxml/signature/parser"
35
+ autoload :ReferenceResolver, "moxml/signature/reference_resolver"
36
+ autoload :Signer, "moxml/signature/signer"
37
+ autoload :Verifier, "moxml/signature/verifier"
38
+ autoload :KeyExtractor, "moxml/signature/key_extractor"
39
+ autoload :TransformPipeline, "moxml/signature/transform_pipeline"
40
+ autoload :VerificationResult, "moxml/signature/verification_result"
41
+ autoload :SingleVerificationResult,
42
+ "moxml/signature/single_verification_result"
43
+ autoload :ReferenceResult, "moxml/signature/reference_result"
44
+
45
+ class << self
46
+ def sign(context:, document:, key:, **options)
47
+ signature = build_signature(context: context, document: document, **options)
48
+ Signer.new(
49
+ context: context,
50
+ signature: signature,
51
+ document: document,
52
+ key: key,
53
+ ).sign
54
+ signature
55
+ end
56
+
57
+ def verify(context:, document:, key: nil, **options)
58
+ Verifier.new(context: context, document: document, key: key, **options).verify
59
+ end
60
+
61
+ private
62
+
63
+ def build_signature(**opts)
64
+ _context = opts[:context]
65
+ _document = opts[:document]
66
+ signed_info = Model::SignedInfo.new(
67
+ canonicalization_method: Model::AlgorithmMethod.new(
68
+ algorithm: opts[:canonicalization_method],
69
+ ),
70
+ signature_method: Model::AlgorithmMethod.new(
71
+ algorithm: opts[:signature_method],
72
+ ),
73
+ references: [
74
+ Model::Reference.new(
75
+ uri: opts[:reference_uri],
76
+ transforms: Model::Transforms.new(
77
+ transforms: opts[:transforms].map do |transform|
78
+ Model::Transform.new(algorithm: transform)
79
+ end,
80
+ ),
81
+ digest_method: Model::DigestMethod.new(algorithm: opts[:digest_method]),
82
+ ),
83
+ ],
84
+ )
85
+
86
+ Model::Signature.new(
87
+ id: opts[:signature_id],
88
+ signed_info: signed_info,
89
+ key_info: opts[:key_info],
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.1.25"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Single source of XML emission knowledge: character escaping,
5
+ # CDATA end-sequence segmentation, and declaration/DOCTYPE
6
+ # rendering. Adapters keep their tree walks (those are
7
+ # adapter-shaped) but stop re-deriving the wire format:
8
+ #
9
+ # - text escapes & < >; attribute values also escape " —
10
+ # apostrophes stay literal (moxml-canonical form)
11
+ # - a CDATA section's end sequence is split into adjacent sections
12
+ # - the moxml declaration shape is <?xml version="..."
13
+ # encoding="..." standalone="..."?> with only the set attributes
14
+ # - DOCTYPE renders PUBLIC and SYSTEM external identifiers
15
+ module XmlEmitter
16
+ TEXT_ESCAPE_RE = /[<>&]/
17
+ TEXT_ESCAPE_MAP = {
18
+ "<" => "&lt;", ">" => "&gt;", "&" => "&amp;"
19
+ }.freeze
20
+ ATTRIBUTE_ESCAPE_RE = /[<>&"]/
21
+ ATTRIBUTE_ESCAPE_MAP = {
22
+ "<" => "&lt;", ">" => "&gt;", "&" => "&amp;", '"' => "&quot;"
23
+ }.freeze
24
+ CDATA_END = "]]>"
25
+ CDATA_END_SPLIT = "]]]]><![CDATA[>"
26
+
27
+ module_function
28
+
29
+ # @return [String] text with & < > escaped
30
+ def escape_text(text)
31
+ str = text.is_a?(String) ? text : text.to_s
32
+ return str unless str.match?(TEXT_ESCAPE_RE)
33
+
34
+ str.gsub(TEXT_ESCAPE_RE, TEXT_ESCAPE_MAP)
35
+ end
36
+
37
+ # @return [String] attribute value with & < > " escaped
38
+ def escape_attribute(value)
39
+ str = value.is_a?(String) ? value : value.to_s
40
+ return str unless str.match?(ATTRIBUTE_ESCAPE_RE)
41
+
42
+ str.gsub(ATTRIBUTE_ESCAPE_RE, ATTRIBUTE_ESCAPE_MAP)
43
+ end
44
+
45
+ # @return [String] a CDATA section, splitting any embedded end
46
+ # sequence into adjacent sections
47
+ def cdata(content)
48
+ "<![CDATA[#{content.to_s.gsub(CDATA_END, CDATA_END_SPLIT)}]]>"
49
+ end
50
+
51
+ # @return [String] an XML declaration with only the set attributes
52
+ def declaration_xml(version, encoding, standalone)
53
+ output = %(<?xml version="#{version}")
54
+ output << %( encoding="#{encoding}") if encoding && !encoding.to_s.empty?
55
+ output << %( standalone="#{standalone}") unless standalone.nil?
56
+ output << "?>"
57
+ end
58
+
59
+ # @return [String] a DOCTYPE with its external identifier
60
+ def doctype_xml(name, public_id, system_id)
61
+ output = "<!DOCTYPE #{name}"
62
+ if public_id && !public_id.to_s.empty?
63
+ output << %( PUBLIC "#{public_id}")
64
+ output << %( "#{system_id}") if system_id
65
+ elsif system_id && !system_id.to_s.empty?
66
+ output << %( SYSTEM "#{system_id}")
67
+ end
68
+ output << ">"
69
+ end
70
+ end
71
+ end
@@ -425,15 +425,22 @@ module Moxml
425
425
  end
426
426
  end
427
427
 
428
- # AXIS: attribute - Enables @attribute syntax
428
+ # AXIS: attribute - Enables @attribute syntax. Name tests share
429
+ # the resolver's expanded-name semantics (Namespaces 1.0): bare
430
+ # names match no-namespace attributes only; prefixed names match
431
+ # by namespace URI, never by prefix spelling.
429
432
  def on_axis_attribute(ast, input)
430
433
  elem_class = const_ref("Moxml", "Element")
431
434
  attribute = unique_literal(:attribute)
432
435
 
433
436
  input.is_a?(elem_class).if_true do
434
437
  input.attributes.each.add_block(attribute) do
435
- # Use process to handle both :test and :wildcard nodes
436
- condition = process(ast, attribute)
438
+ condition = if ast.type == :test
439
+ attribute_test_condition(ast, input, attribute)
440
+ else
441
+ # Wildcards and node types keep the generic path
442
+ process(ast, attribute)
443
+ end
437
444
 
438
445
  if block_given?
439
446
  condition.if_true { yield attribute }
@@ -444,6 +451,34 @@ module Moxml
444
451
  end
445
452
  end
446
453
 
454
+ # Attribute node-test condition built on AttributeResolver. The
455
+ # XPath context's prefix mapping (when provided) wins over the
456
+ # document's own declarations and is resolved at compile time.
457
+ # Returns nil for the both-wildcard test (@*): no name constraint.
458
+ def attribute_test_condition(ast, element, attribute)
459
+ resolver = const_ref("Moxml", "AttributeResolver")
460
+ ns = ast.value[:namespace]
461
+ name = ast.value[:name]
462
+
463
+ return nil if name == STAR && (!ns || ns == STAR)
464
+
465
+ local = name == STAR ? self_nil : string(name)
466
+
467
+ if ns && ns != STAR && @namespaces && (uri = @namespaces[ns])
468
+ return resolver.attribute_test_uri?(element, attribute,
469
+ string(uri.to_s), local)
470
+ end
471
+
472
+ prefix = if ns.nil?
473
+ self_nil
474
+ elsif ns == STAR
475
+ symbol(:any)
476
+ else
477
+ string(ns)
478
+ end
479
+ resolver.attribute_test?(element, attribute, prefix, local)
480
+ end
481
+
447
482
  # AXIS: descendant - All descendant nodes (without self)
448
483
  def on_axis_descendant(ast, input)
449
484
  node = unique_literal(:descendant)
@@ -494,6 +529,13 @@ module Moxml
494
529
  when "node"
495
530
  # node() matches everything — use a truthy literal
496
531
  Ruby::Node.new(:lit, ["true"])
532
+ when "text"
533
+ # CDATA sections are text nodes in the XPath data model
534
+ text_class = const_ref("Moxml", "Text")
535
+ cdata_class = const_ref("Moxml", "Cdata")
536
+ input.is_a?(text_class).or(input.is_a?(cdata_class))
537
+ when "comment"
538
+ input.is_a?(const_ref("Moxml", "Comment"))
497
539
  else element_or_attribute(input)
498
540
  end
499
541
  end
@@ -531,9 +573,21 @@ module Moxml
531
573
  # Match namespace if specified
532
574
  if ns && ns != STAR
533
575
  if @namespaces && @namespaces[ns]
534
- # Resolve prefix to URI using namespace mappings
576
+ # Resolve prefix to URI using namespace mappings. An empty
577
+ # URI binding ("xmlns" => "") selects elements in no
578
+ # namespace, e.g. after an xmlns="" undeclaration.
535
579
  ns_uri = @namespaces[ns]
536
- ns_match = input.namespace.and(input.namespace.uri.eq(string(ns_uri)))
580
+ ns_match = if ns_uri.to_s.empty?
581
+ input.namespace.nil?.or(input.namespace.uri.eq(string("")))
582
+ else
583
+ input.namespace.and(input.namespace.uri.eq(string(ns_uri)))
584
+ end
585
+ elsif ns == "xmlns"
586
+ # Nokogiri-compatible reserved prefix: xmlns:name addresses
587
+ # elements in the default namespace (no prefix).
588
+ ns_match = input.namespace.and(
589
+ input.namespace.prefix.to_s.eq(string("")),
590
+ )
537
591
  else
538
592
  # No mapping provided - check against element's namespace prefix
539
593
  # Need to ensure input.namespace exists first
@@ -977,10 +1031,9 @@ module Moxml
977
1031
  conversions << conversion.to_string(arg_var)
978
1032
  end
979
1033
 
980
- # rubocop:disable Style/RedundantSum, Performance/Sum
1034
+ # rubocop:disable-next Performance/Sum
981
1035
  concatted = assigns.inject(:followed_by)
982
1036
  .followed_by(conversions.inject(:+))
983
- # rubocop:enable Style/RedundantSum, Performance/Sum
984
1037
 
985
1038
  block_given? ? concatted.empty?.if_false { yield concatted } : concatted
986
1039
  end
@@ -327,11 +327,18 @@ module Moxml
327
327
  def parse_relative_path
328
328
  steps = [parse_step]
329
329
 
330
- while match?(:slash) && !at_end?
330
+ # Continue on both step separators: "/step" and the
331
+ # descendant-or-self shorthand "//step". A "//" arrives as a
332
+ # single :dslash (".//step", "a//step") or occasionally as a
333
+ # :slash/:slash or :slash/:dslash pair.
334
+ while !at_end? && match?(:slash, :dslash)
335
+ is_dslash = match?(:dslash)
331
336
  advance
332
- if match?(:slash)
333
- # Double slash within path: expands to descendant-or-self::node()
337
+ if !is_dslash && match?(:slash, :dslash)
334
338
  advance
339
+ is_dslash = true
340
+ end
341
+ if is_dslash
335
342
  steps << AST::Node.axis("descendant-or-self", AST::Node.node_type("node"))
336
343
  end
337
344
  steps << parse_step unless at_end? || match?(:pipe, :rbracket,
@@ -352,14 +359,10 @@ module Moxml
352
359
  return AST::Node.parent
353
360
  elsif match?(:at)
354
361
  advance
355
- # Attribute: @name or @*
356
- if match?(:star)
357
- advance
358
- node_test = AST::Node.wildcard
359
- else
360
- name = consume(:name, "Expected attribute name after @")
361
- node_test = AST::Node.test(nil, name[1])
362
- end
362
+ # Attribute: @name, @prefix:name, @prefix:* or @* (XPath 1.0
363
+ # §3.2, NameTest). parse_node_test handles prefixed and
364
+ # wildcard names; bare names land in its plain-name branch.
365
+ node_test = parse_node_test
363
366
  step = AST::Node.axis("attribute", node_test)
364
367
  return parse_predicates(step)
365
368
  end
@@ -385,14 +388,18 @@ module Moxml
385
388
  if match?(:star)
386
389
  advance
387
390
  return AST::Node.wildcard
388
- elsif match?(:node_type)
391
+ elsif match?(:node_type) && peek_is?(:lparen)
389
392
  type_name = current_value
390
393
  advance
391
394
  consume(:lparen, "Expected '(' after node type")
392
395
  consume(:rparen, "Expected ')' after node type")
393
396
  return AST::Node.node_type(type_name)
394
- elsif match?(:name, :and, :or, :mod, :div)
395
- # Accept keywords as valid element names (they're valid XML names)
397
+ elsif match?(:name, :node_type, :and, :or, :mod, :div)
398
+ # Accept keywords as valid element names (they're valid XML
399
+ # names). Node-type words (text, comment, node) also land here
400
+ # when not followed by '(' — they name elements, either alone
401
+ # ("//text") or as the local part of a prefixed name
402
+ # ("//svg:text").
396
403
  name = current_value
397
404
  advance
398
405
 
@@ -402,7 +409,7 @@ module Moxml
402
409
  if match?(:star)
403
410
  advance
404
411
  return AST::Node.test(name, "*")
405
- elsif match?(:name, :and, :or, :mod, :div)
412
+ elsif match?(:name, :node_type, :and, :or, :mod, :div)
406
413
  # Accept keywords as local names too
407
414
  local_name = current_value
408
415
  advance
data/lib/moxml.rb CHANGED
@@ -76,6 +76,7 @@ module Moxml
76
76
  autoload :Cdata, "moxml/cdata"
77
77
  autoload :Comment, "moxml/comment"
78
78
  autoload :Attribute, "moxml/attribute"
79
+ autoload :AttributeResolver, "moxml/attribute_resolver"
79
80
  autoload :ProcessingInstruction, "moxml/processing_instruction"
80
81
  autoload :Declaration, "moxml/declaration"
81
82
  autoload :Namespace, "moxml/namespace"
@@ -83,12 +84,16 @@ module Moxml
83
84
  autoload :EntityReference, "moxml/entity_reference"
84
85
  autoload :DocumentBuilder, "moxml/document_builder"
85
86
  autoload :Builder, "moxml/builder"
87
+ autoload :Entity, "moxml/entity"
86
88
  autoload :EntityRegistry, "moxml/entity_registry"
87
89
  autoload :NativeAttachment, "moxml/native_attachment"
88
90
  autoload :XmlUtils, "moxml/xml_utils"
91
+ autoload :XmlEmitter, "moxml/xml_emitter"
89
92
  autoload :Adapter, "moxml/adapter"
90
93
  autoload :XPath, "moxml/xpath"
91
94
  autoload :SAX, "moxml/sax"
95
+ autoload :Signature, "moxml/signature"
96
+ autoload :C14n, "moxml/c14n"
92
97
 
93
98
  # Error hierarchy — each subclass autoloads from the same file
94
99
  autoload :Error, "moxml/error"