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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- 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 +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -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/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- 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 +28 -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/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- 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/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -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/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -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/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- 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
|
@@ -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
|
+
"<" => "<", ">" => ">", "&" => "&"
|
|
19
|
+
}.freeze
|
|
20
|
+
ATTRIBUTE_ESCAPE_RE = /[<>&"]/
|
|
21
|
+
ATTRIBUTE_ESCAPE_MAP = {
|
|
22
|
+
"<" => "<", ">" => ">", "&" => "&", '"' => """
|
|
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
|
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -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
|
-
|
|
436
|
-
|
|
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 =
|
|
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
|
|
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
|
data/lib/moxml/xpath/parser.rb
CHANGED
|
@@ -327,11 +327,18 @@ module Moxml
|
|
|
327
327
|
def parse_relative_path
|
|
328
328
|
steps = [parse_step]
|
|
329
329
|
|
|
330
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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"
|