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,143 @@
|
|
|
1
|
+
# Security considerations
|
|
2
|
+
|
|
3
|
+
XML signature is a complex spec with a long history of attacks. This
|
|
4
|
+
document explains what `Moxml::Signature` does to mitigate them and
|
|
5
|
+
what the application is responsible for.
|
|
6
|
+
|
|
7
|
+
## Built-in mitigations
|
|
8
|
+
|
|
9
|
+
### Best Practice 1 — authenticate before transforms (DONE)
|
|
10
|
+
|
|
11
|
+
The Verifier validates `SignatureValue` before running any reference
|
|
12
|
+
transforms. A hostile signature with expensive XSLT or XPath transforms
|
|
13
|
+
cannot consume server resources unless its `SignatureValue` verifies
|
|
14
|
+
against a key the application trusts.
|
|
15
|
+
|
|
16
|
+
### Best Practice 26 — HMAC truncation floor (DONE)
|
|
17
|
+
|
|
18
|
+
`HMACOutputLength` values below `max(hash_bits/2, 80)` are rejected
|
|
19
|
+
during algorithm instantiation (spec §4.4.2). Signatures with
|
|
20
|
+
sub-minimum truncation are deemed invalid.
|
|
21
|
+
|
|
22
|
+
### Best Practice 11 — opaque certificate handling (DONE)
|
|
23
|
+
|
|
24
|
+
`X509Certificate` payloads are decoded as raw DER and passed to
|
|
25
|
+
`OpenSSL::X509::Certificate.new`. We never re-encode certificates, so
|
|
26
|
+
the signature on the certificate itself is preserved.
|
|
27
|
+
|
|
28
|
+
## What the application must do
|
|
29
|
+
|
|
30
|
+
The library cannot make trust decisions; the application must.
|
|
31
|
+
|
|
32
|
+
### Best Practice 2 — establish trust in the key
|
|
33
|
+
|
|
34
|
+
Just because `SignatureValue` verifies against a public key in
|
|
35
|
+
`KeyInfo` does NOT mean the signature should be trusted. The key must
|
|
36
|
+
come from a trusted source:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Application-side: pin the expected key, ignore KeyInfo entirely
|
|
40
|
+
result = Moxml::Signature.verify(
|
|
41
|
+
context: ctx,
|
|
42
|
+
document: doc,
|
|
43
|
+
key: trusted_public_key, # do NOT rely on KeyInfo extraction
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Or, with certificate validation:
|
|
47
|
+
cert = OpenSSL::X509::Certificate.new(cert_der)
|
|
48
|
+
store = OpenSSL::X509::Store.new
|
|
49
|
+
store.add_trust_file("cacert.pem")
|
|
50
|
+
unless store.verify(cert)
|
|
51
|
+
raise "certificate chain invalid"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = Moxml::Signature.verify(
|
|
55
|
+
context: ctx,
|
|
56
|
+
document: doc,
|
|
57
|
+
key: cert.public_key,
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Best Practice 12 — see what was signed
|
|
62
|
+
|
|
63
|
+
Use `SingleVerificationResult#references` to inspect what the signature
|
|
64
|
+
actually covers:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
result.results.first.references.each do |ref|
|
|
68
|
+
puts "#{ref.uri}: #{ref.valid?}"
|
|
69
|
+
# Confirm ref.uri matches what the application expects to be signed.
|
|
70
|
+
# Wrapping attacks work by getting the verifier to confirm a different
|
|
71
|
+
# node than the application acts on.
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Best Practice 14 — check name AND position
|
|
76
|
+
|
|
77
|
+
When checking a reference URI, don't just verify the element name. A
|
|
78
|
+
wrapping attack moves the signed element into an `<Object>` and points
|
|
79
|
+
the reference at it; the application then acts on a different (unsigned)
|
|
80
|
+
element with the same name.
|
|
81
|
+
|
|
82
|
+
### Best Practice 8 — control external references
|
|
83
|
+
|
|
84
|
+
External URI dereferencing is **not enabled by default**. If you need
|
|
85
|
+
it, wrap the resolver in a policy that:
|
|
86
|
+
|
|
87
|
+
- Allows only same-document URIs (`#id`, `""`)
|
|
88
|
+
- Allows only specific schemes (`https://`, never `file://`)
|
|
89
|
+
- Caps size and timeout
|
|
90
|
+
- Disallows query parameters that mutate server state
|
|
91
|
+
|
|
92
|
+
## What this library does NOT do
|
|
93
|
+
|
|
94
|
+
### Not a chain validator
|
|
95
|
+
|
|
96
|
+
`Moxml::Signature` does not validate X.509 certificate chains, check
|
|
97
|
+
revocation, or evaluate certificate policies. The application is
|
|
98
|
+
responsible for these (see Best Practice 2 above).
|
|
99
|
+
|
|
100
|
+
### Not a timestamp authority
|
|
101
|
+
|
|
102
|
+
Long-lived signatures need RFC 3161 timestamps from a TSA. This library
|
|
103
|
+
does not implement timestamp verification.
|
|
104
|
+
|
|
105
|
+
### Not a wrapping-attack detector
|
|
106
|
+
|
|
107
|
+
Wrapping attacks succeed when the application acts on a different node
|
|
108
|
+
than the one the signature actually covers. The library returns the
|
|
109
|
+
list of references; the application must verify they cover the right
|
|
110
|
+
nodes.
|
|
111
|
+
|
|
112
|
+
## SHA-1 warning
|
|
113
|
+
|
|
114
|
+
The W3C spec marks SHA-1 as REQUIRED for backwards compatibility but
|
|
115
|
+
DISCOURAGED for new signatures. Cryptanalytic advances (SHAttered,
|
|
116
|
+
2017) demonstrated practical collisions. Use SHA-256 or stronger for
|
|
117
|
+
new signatures.
|
|
118
|
+
|
|
119
|
+
## HMAC security
|
|
120
|
+
|
|
121
|
+
HMAC signatures require a shared secret. Any verifier with the secret
|
|
122
|
+
can forge signatures. Use distinct keys for signing vs. encryption
|
|
123
|
+
(Best Practice 27).
|
|
124
|
+
|
|
125
|
+
## Limited XPath support
|
|
126
|
+
|
|
127
|
+
The library implements the Enveloped Signature transform directly
|
|
128
|
+
(no XPath). XPath Filter and XSLT transforms are not implemented;
|
|
129
|
+
Best Practices 3, 5, 6, 22 say avoid them.
|
|
130
|
+
|
|
131
|
+
## Performance and DoS
|
|
132
|
+
|
|
133
|
+
Canonicalization is O(N) for tree size. XSLT and complex XPath are
|
|
134
|
+
not supported, eliminating the most common DoS vectors (Best Practices
|
|
135
|
+
3, 5, 6). Wrap calls in a timeout if processing untrusted input:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
require "timeout"
|
|
139
|
+
|
|
140
|
+
Timeout.timeout(5) do
|
|
141
|
+
Moxml::Signature.verify(context: ctx, document: untrusted_doc, key:)
|
|
142
|
+
end
|
|
143
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: auto-extract the verification key from a KeyInfo that
|
|
4
|
+
# contains an embedded X509Certificate. The Verifier derives the
|
|
5
|
+
# OpenSSL key without application help.
|
|
6
|
+
#
|
|
7
|
+
# Run with: bundle exec ruby examples/signature/auto_key_extraction.rb
|
|
8
|
+
|
|
9
|
+
require "moxml"
|
|
10
|
+
require "moxml/signature"
|
|
11
|
+
|
|
12
|
+
ctx = Moxml.new(:nokogiri)
|
|
13
|
+
|
|
14
|
+
fixture = File.expand_path(
|
|
15
|
+
"../../spec/fixtures/xmldsig/sign3-result.xml",
|
|
16
|
+
__dir__,
|
|
17
|
+
)
|
|
18
|
+
xml = File.read(fixture)
|
|
19
|
+
doc = ctx.parse(xml)
|
|
20
|
+
|
|
21
|
+
# No explicit key — Verifier auto-extracts from X509Certificate.
|
|
22
|
+
result = Moxml::Signature.verify(context: ctx, document: doc)
|
|
23
|
+
puts "Auto-extracted from X509Certificate: #{result.valid?}"
|
|
24
|
+
puts "Signature count: #{result.signature_count}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: enveloped RSA-SHA256 signature with verification.
|
|
4
|
+
#
|
|
5
|
+
# Run with: bundle exec ruby examples/signature/enveloped_rsa.rb
|
|
6
|
+
|
|
7
|
+
require "moxml"
|
|
8
|
+
require "moxml/signature"
|
|
9
|
+
require "openssl"
|
|
10
|
+
|
|
11
|
+
ctx = Moxml.new(:nokogiri)
|
|
12
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
13
|
+
|
|
14
|
+
doc = ctx.parse("<doc><greeting>Hello, World!</greeting></doc>")
|
|
15
|
+
puts "=== Before signing ==="
|
|
16
|
+
puts doc.to_xml
|
|
17
|
+
|
|
18
|
+
signature = Moxml::Signature.sign(
|
|
19
|
+
context: ctx,
|
|
20
|
+
document: doc,
|
|
21
|
+
key: key,
|
|
22
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
23
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
24
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
25
|
+
reference_uri: "",
|
|
26
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
|
|
30
|
+
doc.root.add_child(serialized.root)
|
|
31
|
+
|
|
32
|
+
puts ""
|
|
33
|
+
puts "=== After signing ==="
|
|
34
|
+
puts doc.to_xml
|
|
35
|
+
|
|
36
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
37
|
+
puts ""
|
|
38
|
+
puts "=== Verification ==="
|
|
39
|
+
puts "Valid: #{result.valid?}"
|
|
40
|
+
|
|
41
|
+
# Tamper test
|
|
42
|
+
doc2 = ctx.parse(doc.to_xml(indent: 0))
|
|
43
|
+
doc2.at_xpath("//greeting").text = "Goodbye!"
|
|
44
|
+
tampered = Moxml::Signature.verify(context: ctx, document: doc2, key: key)
|
|
45
|
+
puts "Tampered: #{tampered.valid?} (expected false)"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: HMAC-SHA256 signature with a shared secret.
|
|
4
|
+
#
|
|
5
|
+
# Run with: bundle exec ruby examples/signature/hmac.rb
|
|
6
|
+
|
|
7
|
+
require "moxml"
|
|
8
|
+
require "moxml/signature"
|
|
9
|
+
|
|
10
|
+
ctx = Moxml.new(:nokogiri)
|
|
11
|
+
secret = "super-secret-shared-key"
|
|
12
|
+
|
|
13
|
+
doc = ctx.parse("<message><body>hello</body></message>")
|
|
14
|
+
|
|
15
|
+
signature = Moxml::Signature.sign(
|
|
16
|
+
context: ctx,
|
|
17
|
+
document: doc,
|
|
18
|
+
key: secret,
|
|
19
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
|
20
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
21
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
22
|
+
reference_uri: "",
|
|
23
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
|
|
27
|
+
doc.root.add_child(serialized.root)
|
|
28
|
+
|
|
29
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: secret)
|
|
30
|
+
puts "Valid: #{result.valid?}"
|
|
31
|
+
|
|
32
|
+
wrong = Moxml::Signature.verify(context: ctx, document: doc, key: "wrong")
|
|
33
|
+
puts "Wrong secret: #{wrong.valid?} (expected false)"
|
|
@@ -24,6 +24,9 @@ require "moxml/namespace"
|
|
|
24
24
|
require "moxml/doctype"
|
|
25
25
|
require "moxml/entity_reference"
|
|
26
26
|
require "moxml/entity_registry"
|
|
27
|
+
require "moxml/entity"
|
|
28
|
+
require "moxml/entity/reference"
|
|
29
|
+
require "moxml/xml_emitter"
|
|
27
30
|
require "moxml/adapter"
|
|
28
31
|
require "moxml/adapter/base"
|
|
29
32
|
require "moxml/adapter/customized_rexml"
|
|
@@ -36,6 +39,8 @@ require "moxml/sax/block_handler"
|
|
|
36
39
|
require "moxml/sax/namespace_splitter"
|
|
37
40
|
require "moxml/adapter/rexml"
|
|
38
41
|
require "moxml/adapter/customized_oga"
|
|
42
|
+
require "moxml/adapter/customized_oga/entity_decoder"
|
|
43
|
+
require "moxml/adapter/customized_oga/raw_value_override"
|
|
39
44
|
require "moxml/adapter/customized_oga/xml_declaration"
|
|
40
45
|
require "moxml/adapter/customized_oga/xml_generator"
|
|
41
46
|
require "moxml/adapter/oga"
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -5,62 +5,29 @@ module Moxml
|
|
|
5
5
|
class Base
|
|
6
6
|
# include XmlUtils
|
|
7
7
|
|
|
8
|
-
# Entity
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
ENTITY_NAME_RE = /&(#{ENTITY_NAME_PATTERN});/
|
|
18
|
-
ENTITY_MARKER_RE = /\u{FFFC}\u{FEFF}(#{ENTITY_NAME_PATTERN});/
|
|
19
|
-
SERIALIZED_ENTITY_MARKER_RE = /(#{ENTITY_NAME_PATTERN});/
|
|
20
|
-
STANDARD_ENTITIES = %w[amp lt gt quot apos].freeze
|
|
8
|
+
# Entity round-trip pipeline lives in Moxml::Entity; these
|
|
9
|
+
# constants and methods stay on the adapter protocol so
|
|
10
|
+
# adapters and wrappers keep their calling convention.
|
|
11
|
+
ENTITY_MARKER = Entity::MARKER
|
|
12
|
+
ENTITY_NAME_PATTERN = Entity::NAME_PATTERN
|
|
13
|
+
ENTITY_NAME_RE = Entity::NAME_RE
|
|
14
|
+
ENTITY_MARKER_RE = Entity::MARKER_RE
|
|
15
|
+
SERIALIZED_ENTITY_MARKER_RE = Entity::SERIALIZED_MARKER_RE
|
|
16
|
+
STANDARD_ENTITIES = Entity::STANDARD_ENTITIES
|
|
21
17
|
|
|
22
18
|
class << self
|
|
23
19
|
include XmlUtils
|
|
24
20
|
|
|
25
|
-
# Replace non-standard entity references with markers before parsing.
|
|
26
|
-
# Always returns a UTF-8 encoded string.
|
|
27
21
|
def preprocess_entities(xml)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
str = if xml.encoding == Encoding::BINARY
|
|
31
|
-
# Binary strings are assumed to be UTF-8. If the bytes are
|
|
32
|
-
# not valid UTF-8, fall back to encoding as UTF-8 with
|
|
33
|
-
# replacement to avoid raising on gsub.
|
|
34
|
-
dup = xml.dup.force_encoding("UTF-8")
|
|
35
|
-
if dup.valid_encoding?
|
|
36
|
-
dup
|
|
37
|
-
else
|
|
38
|
-
xml.dup.encode("UTF-8",
|
|
39
|
-
"ASCII-8BIT", invalid: :replace, undef: :replace)
|
|
40
|
-
end
|
|
41
|
-
elsif xml.encoding == Encoding::UTF_8
|
|
42
|
-
xml
|
|
43
|
-
else
|
|
44
|
-
xml.encode("UTF-8")
|
|
45
|
-
end
|
|
46
|
-
# Fast path: no `&` means no entity references to mark — skip
|
|
47
|
-
# the regex scan and string allocation entirely. The vast
|
|
48
|
-
# majority of XML payloads contain no entity references.
|
|
49
|
-
return str unless str.include?("&")
|
|
50
|
-
|
|
51
|
-
str.gsub(ENTITY_NAME_RE) do |match|
|
|
52
|
-
STANDARD_ENTITIES.include?(::Regexp.last_match(1)) ? match : "#{ENTITY_MARKER}#{::Regexp.last_match(1)};"
|
|
53
|
-
end
|
|
22
|
+
Entity.preprocess_entities(xml)
|
|
54
23
|
end
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
def decode_entities(text)
|
|
26
|
+
Entity.decode_entities(text)
|
|
27
|
+
end
|
|
59
28
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
result = str.gsub(ENTITY_MARKER_RE, '&\1;')
|
|
63
|
-
result.gsub(SERIALIZED_ENTITY_MARKER_RE, '&\1;')
|
|
29
|
+
def restore_entities(text)
|
|
30
|
+
Entity.restore_entities(text)
|
|
64
31
|
end
|
|
65
32
|
|
|
66
33
|
def set_root(_doc, _element)
|
|
@@ -71,7 +38,7 @@ module Moxml
|
|
|
71
38
|
)
|
|
72
39
|
end
|
|
73
40
|
|
|
74
|
-
def parse(_xml, _options = {})
|
|
41
|
+
def parse(_xml, _options = {}, _context = nil)
|
|
75
42
|
raise Moxml::NotImplementedError.new(
|
|
76
43
|
"parse not implemented",
|
|
77
44
|
feature: "parse",
|
|
@@ -172,12 +139,35 @@ namespace_validation_mode: :strict)
|
|
|
172
139
|
create_native_entity_reference(name)
|
|
173
140
|
end
|
|
174
141
|
|
|
142
|
+
# Mutation return contract: protocol methods that may change
|
|
143
|
+
# which native a wrapper tracks (set_attribute_name,
|
|
144
|
+
# set_namespace, set_attribute_value) always return the native
|
|
145
|
+
# the wrapper must keep tracking — the same object when
|
|
146
|
+
# mutated in place, a fresh object when the adapter recreates
|
|
147
|
+
# the node.
|
|
175
148
|
def set_attribute_name(attribute, name)
|
|
176
149
|
attribute.name = name
|
|
150
|
+
attribute
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def set_namespace(_node, _namespace)
|
|
154
|
+
raise Moxml::NotImplementedError.new(
|
|
155
|
+
"set_namespace not implemented",
|
|
156
|
+
feature: "set_namespace",
|
|
157
|
+
adapter: name,
|
|
158
|
+
)
|
|
177
159
|
end
|
|
178
160
|
|
|
179
161
|
def set_attribute_value(attribute, value)
|
|
180
162
|
attribute.value = value
|
|
163
|
+
attribute
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Remove a specific native attribute node from its owning
|
|
167
|
+
# element. Semantics (which attribute a name addresses) live
|
|
168
|
+
# in Moxml::AttributeResolver; this is the raw primitive.
|
|
169
|
+
def remove_attribute_native(attr)
|
|
170
|
+
attr.remove
|
|
181
171
|
end
|
|
182
172
|
|
|
183
173
|
def entity_reference_name(node)
|
|
@@ -212,6 +202,20 @@ namespace_validation_mode: :strict)
|
|
|
212
202
|
# Called when a Declaration node is removed from a document.
|
|
213
203
|
def remove_declaration(_native_doc); end
|
|
214
204
|
|
|
205
|
+
# Source line of a native node (1-based), or nil when the
|
|
206
|
+
# underlying backend does not track source positions.
|
|
207
|
+
# Adapters that track lines (Nokogiri, LibXML) override this.
|
|
208
|
+
def line_number(_node)
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Local name of a native attribute node. Adapters whose natives
|
|
213
|
+
# carry qualified names override this to expose the local part;
|
|
214
|
+
# the wrapper composes the prefix.
|
|
215
|
+
def attribute_name(attr)
|
|
216
|
+
attr.name
|
|
217
|
+
end
|
|
218
|
+
|
|
215
219
|
# Return the actual native node after an add_child operation.
|
|
216
220
|
# Override for adapters where node identity may change (e.g., LibXML doc.root=).
|
|
217
221
|
def actual_native(child_native, _parent_native)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
module CustomizedLeptris
|
|
6
|
+
# Wrapper for an XML declaration. libleptris stores declaration
|
|
7
|
+
# data on the document struct (read-only, serialized via options)
|
|
8
|
+
# and has no first-class declaration node, so Moxml exposes this
|
|
9
|
+
# mutable value object instead.
|
|
10
|
+
class Declaration
|
|
11
|
+
attr_accessor :version, :encoding, :standalone, :parent_doc
|
|
12
|
+
|
|
13
|
+
def initialize(version = "1.0", encoding = "UTF-8", standalone = nil)
|
|
14
|
+
@version = version
|
|
15
|
+
@encoding = encoding
|
|
16
|
+
@standalone = standalone
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_xml
|
|
20
|
+
XmlEmitter.declaration_xml(version, encoding, standalone)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(self.class) &&
|
|
25
|
+
version == other.version &&
|
|
26
|
+
encoding == other.encoding &&
|
|
27
|
+
standalone == other.standalone
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
module CustomizedLeptris
|
|
6
|
+
# Wrapper for a programmatic DOCTYPE. libleptris only parses
|
|
7
|
+
# DOCTYPEs from source; it has no API to create one, so Moxml
|
|
8
|
+
# stores this value object in the document's attachments.
|
|
9
|
+
class Doctype
|
|
10
|
+
attr_accessor :name, :external_id, :system_id, :parent_doc
|
|
11
|
+
|
|
12
|
+
def initialize(name, external_id = nil, system_id = nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@external_id = external_id
|
|
15
|
+
@system_id = system_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_xml
|
|
19
|
+
XmlEmitter.doctype_xml(name, external_id, system_id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(self.class) &&
|
|
24
|
+
name == other.name &&
|
|
25
|
+
external_id == other.external_id &&
|
|
26
|
+
system_id == other.system_id
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
module CustomizedLeptris
|
|
6
|
+
# libleptris expands the five built-in entities at parse time
|
|
7
|
+
# and has no custom ones; the adapter carries entity references
|
|
8
|
+
# as Moxml::Entity::Reference values over marker-bearing text.
|
|
9
|
+
EntityReference = ::Moxml::Entity::Reference
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
module CustomizedLeptris
|
|
6
|
+
# A text run split out of a native text node that carried entity
|
|
7
|
+
# markers. Value object: the moxml contract exposes text nodes as
|
|
8
|
+
# separate children around entity references, while libleptris
|
|
9
|
+
# stores them as one marker-bearing text node.
|
|
10
|
+
class TextSegment
|
|
11
|
+
attr_accessor :parent
|
|
12
|
+
|
|
13
|
+
attr_reader :content
|
|
14
|
+
|
|
15
|
+
def initialize(content, parent = nil)
|
|
16
|
+
@content = content
|
|
17
|
+
@parent = parent
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.is_a?(self.class) && content == other.content
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
# Pure-Ruby node kinds that libleptris has no native representation
|
|
6
|
+
# for: XML declarations, programmatic DOCTYPEs, and entity references.
|
|
7
|
+
# The adapter stores them via NativeAttachment and serializes them
|
|
8
|
+
# from #to_xml.
|
|
9
|
+
module CustomizedLeptris
|
|
10
|
+
autoload :Declaration, "moxml/adapter/customized_leptris/declaration"
|
|
11
|
+
autoload :Doctype, "moxml/adapter/customized_leptris/doctype"
|
|
12
|
+
autoload :EntityReference, "moxml/adapter/customized_leptris/entity_reference"
|
|
13
|
+
autoload :TextSegment, "moxml/adapter/customized_leptris/text_segment"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -5,19 +5,10 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML CDATA section nodes
|
|
7
7
|
class Cdata < Node
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# libxml stores CDATA payload verbatim. Only the `]]>` end-marker
|
|
9
|
+
# needs splitting before re-wrapping.
|
|
10
10
|
def to_xml
|
|
11
|
-
|
|
12
|
-
.gsub(""", '"')
|
|
13
|
-
.gsub("'", "'")
|
|
14
|
-
.gsub("<", "<")
|
|
15
|
-
.gsub(">", ">")
|
|
16
|
-
.gsub("&", "&")
|
|
17
|
-
|
|
18
|
-
# Handle CDATA end marker escaping (]]> becomes ]]]]><![CDATA[>)
|
|
19
|
-
# Replace all ]]> markers in the content before wrapping
|
|
20
|
-
escaped_content = content.gsub("]]>", "]]]]><![CDATA[>")
|
|
11
|
+
escaped_content = @native.content.gsub("]]>", "]]]]><![CDATA[>")
|
|
21
12
|
"<![CDATA[#{escaped_content}]]>"
|
|
22
13
|
end
|
|
23
14
|
end
|
|
@@ -5,16 +5,9 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML comment nodes
|
|
7
7
|
class Comment < Node
|
|
8
|
-
#
|
|
9
|
-
# LibXML auto-escapes content, we need to un-escape it
|
|
8
|
+
# libxml stores comment payload verbatim.
|
|
10
9
|
def to_xml
|
|
11
|
-
|
|
12
|
-
.gsub(""", '"')
|
|
13
|
-
.gsub("'", "'")
|
|
14
|
-
.gsub("<", "<")
|
|
15
|
-
.gsub(">", ">")
|
|
16
|
-
.gsub("&", "&")
|
|
17
|
-
"<!--#{content}-->"
|
|
10
|
+
"<!--#{@native.content}-->"
|
|
18
11
|
end
|
|
19
12
|
end
|
|
20
13
|
end
|
|
@@ -47,16 +47,7 @@ module Moxml
|
|
|
47
47
|
|
|
48
48
|
# Generate XML declaration string
|
|
49
49
|
def to_xml
|
|
50
|
-
|
|
51
|
-
if @encoding && !@encoding.empty?
|
|
52
|
-
output << " encoding=\"#{@encoding}\""
|
|
53
|
-
end
|
|
54
|
-
# Include standalone attribute if explicitly set (true or false)
|
|
55
|
-
unless @standalone_value.nil?
|
|
56
|
-
output << " standalone=\"#{standalone}\""
|
|
57
|
-
end
|
|
58
|
-
output << "?>"
|
|
59
|
-
output
|
|
50
|
+
XmlEmitter.declaration_xml(@version, @encoding, standalone)
|
|
60
51
|
end
|
|
61
52
|
|
|
62
53
|
private
|
|
@@ -18,6 +18,14 @@ module Moxml
|
|
|
18
18
|
@native = native_node
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Swap the wrapped native node. Used by the Libxml adapter when
|
|
22
|
+
# libxml-ruby's content= setter would silently re-escape stored
|
|
23
|
+
# text; replacing the node with a fresh raw-storage instance is
|
|
24
|
+
# the only way to preserve verbatim content.
|
|
25
|
+
def replace_native!(fresh)
|
|
26
|
+
@native = fresh
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
# Compare wrappers based on their native nodes
|
|
22
30
|
def ==(other)
|
|
23
31
|
return false unless other
|
|
@@ -5,20 +5,12 @@ module Moxml
|
|
|
5
5
|
module CustomizedLibxml
|
|
6
6
|
# Wrapper for LibXML processing instruction nodes
|
|
7
7
|
class ProcessingInstruction < Node
|
|
8
|
-
#
|
|
9
|
-
# LibXML auto-escapes content, we need to un-escape it
|
|
8
|
+
# XML 1.0 §2.6: PI content is verbatim — no entity resolution, no escaping.
|
|
10
9
|
def to_xml
|
|
11
10
|
target = @native.name
|
|
12
11
|
content = @native.content
|
|
13
|
-
|
|
14
|
-
# Un-escape LibXML's automatic escaping
|
|
15
12
|
if content && !content.empty?
|
|
16
|
-
|
|
17
|
-
.gsub("'", "'")
|
|
18
|
-
.gsub("<", "<")
|
|
19
|
-
.gsub(">", ">")
|
|
20
|
-
.gsub("&", "&")
|
|
21
|
-
"<?#{target} #{unescaped}?>"
|
|
13
|
+
"<?#{target} #{content}?>"
|
|
22
14
|
else
|
|
23
15
|
"<?#{target}?>"
|
|
24
16
|
end
|
|
@@ -13,16 +13,9 @@ module Moxml
|
|
|
13
13
|
@native.content
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# LibXML's .content already contains escaped text, but it over-escapes
|
|
18
|
-
# quotes which don't need escaping in text nodes (only in attributes)
|
|
16
|
+
# @native.to_s escapes & < > but leaves quotes alone, which text nodes need.
|
|
19
17
|
def to_xml
|
|
20
|
-
|
|
21
|
-
# Skip the gsub allocation entirely when there's nothing to undo —
|
|
22
|
-
# the common case for parsed text without literal quotes.
|
|
23
|
-
return content unless content.include?(""")
|
|
24
|
-
|
|
25
|
-
content.gsub(""", '"')
|
|
18
|
+
@native.to_s
|
|
26
19
|
end
|
|
27
20
|
end
|
|
28
21
|
end
|