moxml 0.5.117 → 0.5.118
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/Gemfile +1 -1
- data/README.adoc +1 -1
- data/docs/c14n/README.md +39 -0
- data/docs/c14n/algorithms.md +65 -0
- data/docs/c14n/api.md +62 -0
- data/docs/c14n/examples.md +47 -0
- data/docs/signature/c14n.md +8 -6
- data/lib/moxml/adapter/leptris.rb +16 -4
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/plan_conformance_spec.rb +24 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60a08183c240700f3cde7675fb3428b47340f0dba11e723309c5afb09aa988bb
|
|
4
|
+
data.tar.gz: 42e790020feb09861560717610f16f6e8996fb7435689eae1f989a05cb90ab0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4accde9025cb43cbe09661c68be8d31047f39339f035f2a80d895e3e8b03a786b7b0d492880a39d0f2f4b57b0209002e62cc79c7c71b3c4373091ceaac4481ee
|
|
7
|
+
data.tar.gz: 203fb7db9d8e46a7a028218dda254367cc0cb1c8ac096b516102937874ba0f14b2bbe1f06cfcb413334567722d31f94f4e00fe2850e2c3a20f1334a3bfce5f70
|
data/Gemfile
CHANGED
data/README.adoc
CHANGED
|
@@ -119,7 +119,7 @@ carries the plan-ABI surface, the 1.9.290 XSLT-params and
|
|
|
119
119
|
bulk-append faces, and the 1.9.292 engine deep-copy root install).
|
|
120
120
|
|===
|
|
121
121
|
|
|
122
|
-
NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.
|
|
122
|
+
NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.320` (the tested pair; the gemspec itself has zero runtime dependencies). Consumers of the leptris adapter should run leptris 1.9.320+ — earlier engines carry silent wrong-answer plan-path defects (moxml#344); moxml gates degrade to the always-correct slower paths below it.
|
|
123
123
|
`ox ~> 2.14`, `libxml-ruby >= 5.0`, and the vendored oga fork — the CI
|
|
124
124
|
matrix tests exactly those floors.
|
|
125
125
|
|
data/docs/c14n/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Canonicalization (C14N)
|
|
2
|
+
|
|
3
|
+
Canonicalization is the load-bearing primitive for XML signature and a
|
|
4
|
+
top-level moxml feature, sibling to `Moxml::XPath`, `Moxml::Builder`,
|
|
5
|
+
and `Moxml::SAX`. Two documents that differ only in surface
|
|
6
|
+
representation (whitespace, attribute order, namespace prefix choice)
|
|
7
|
+
produce identical canonical bytes.
|
|
8
|
+
|
|
9
|
+
## Pages
|
|
10
|
+
|
|
11
|
+
- [Algorithms](algorithms.md) — Inclusive C14N 1.0/1.1 (canon-port) and
|
|
12
|
+
Exclusive C14N 1.0 (moxml-native), the data model, output invariants,
|
|
13
|
+
and adapter delegation.
|
|
14
|
+
- [API](api.md) — the full public surface: canonicalize, exclusive,
|
|
15
|
+
subset, digests, and the comparison helper.
|
|
16
|
+
- [Examples](examples.md) — patterns: whole-document, subset,
|
|
17
|
+
cross-adapter verification, digests.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Canonical octets (inclusive C14N 1.0)
|
|
23
|
+
Moxml::C14n.canonicalize(node_or_xml)
|
|
24
|
+
|
|
25
|
+
# Exclusive C14N 1.0 (signatures over moved subdocuments)
|
|
26
|
+
Moxml::C14n.canonicalize_exclusive(node_or_xml,
|
|
27
|
+
inclusive_namespaces: ["foo"])
|
|
28
|
+
|
|
29
|
+
# Node-set subset canonicalization (same-document references)
|
|
30
|
+
Moxml::C14n.canonicalize_subset(node_or_xml, "//signed")
|
|
31
|
+
|
|
32
|
+
# Canonical comparison (testing, regression, signature debugging)
|
|
33
|
+
Moxml::C14n.same_canonical_form?(left, right)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`node_or_xml` accepts a `Moxml::Node`, `Moxml::Document`, or XML
|
|
37
|
+
`String`. Engines without a native canonicalizer take the pure-Ruby
|
|
38
|
+
path — results are byte-identical across adapters, which is what keeps
|
|
39
|
+
cross-adapter signature verification sound.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# C14N algorithms
|
|
2
|
+
|
|
3
|
+
## Inclusive C14N 1.0 / 1.1
|
|
4
|
+
|
|
5
|
+
- W3C: <https://www.w3.org/TR/xml-c14n/>, <https://www.w3.org/TR/xml-c14n11/>
|
|
6
|
+
- Implementation: **ported from `lutaml/canon`** (~1,200 lines)
|
|
7
|
+
- Files: `lib/moxml/c14n/inclusive_10.rb`, `inclusive_11.rb`,
|
|
8
|
+
`data_model.rb`, `processor.rb`, `namespace_handler.rb`,
|
|
9
|
+
`attribute_handler.rb`, `xml_base_handler.rb`, `character_encoder.rb`,
|
|
10
|
+
`node.rb`, `nodes/*.rb`
|
|
11
|
+
- "Attracts" ancestor context: at the apex, every in-scope namespace is
|
|
12
|
+
rendered, including those inherited from outside the canonicalization
|
|
13
|
+
subset.
|
|
14
|
+
|
|
15
|
+
## Exclusive C14N 1.0
|
|
16
|
+
|
|
17
|
+
- W3C: <https://www.w3.org/TR/xml-exc-c14n/>
|
|
18
|
+
- Implementation: **moxml-native** (canon does not implement exclusive)
|
|
19
|
+
- Files: `lib/moxml/c14n/exclusive.rb`, `writer.rb`, `namespace_context.rb`
|
|
20
|
+
- "Repels" ancestor context: only namespaces visibly used by the apex
|
|
21
|
+
element's qualified name or attributes are rendered. Keeps signatures
|
|
22
|
+
valid when subdocuments move between XML contexts (e.g., into a SOAP
|
|
23
|
+
envelope).
|
|
24
|
+
|
|
25
|
+
## Adapter delegation
|
|
26
|
+
|
|
27
|
+
Adapters whose engines ship a native canonicalizer delegate through a
|
|
28
|
+
lazy byte-safety probe — the pure-Ruby path remains the fallback and
|
|
29
|
+
the arbiter: all routes produce byte-identical output (pinned by
|
|
30
|
+
`spec/moxml/c14n/adapter_delegation_spec.rb`).
|
|
31
|
+
|
|
32
|
+
- **leptris**: engine-delegated inclusive C14N — an order of magnitude
|
|
33
|
+
faster than the Ruby path on large documents.
|
|
34
|
+
- **nokogiri**: libxml2 native canonicalization.
|
|
35
|
+
|
|
36
|
+
## Data model
|
|
37
|
+
|
|
38
|
+
The canon-derived inclusive engines walk an intermediate data model
|
|
39
|
+
(`Moxml::C14n::Nodes::*`) rather than the live `Moxml::Node` tree,
|
|
40
|
+
because canonicalization needs:
|
|
41
|
+
|
|
42
|
+
- **Node-set membership flags** for subset canonicalization (spec §3).
|
|
43
|
+
- **Sorted namespace and attribute axes** per spec §2.3 / §2.4.
|
|
44
|
+
- **xml:base fixup** per RFC 3986 with C14N 1.1 modifications.
|
|
45
|
+
- **xml:\* inheritable attribute resolution** (xml:lang, xml:space)
|
|
46
|
+
from omitted ancestors.
|
|
47
|
+
|
|
48
|
+
The data model is built from `Moxml::Node` via `Moxml::C14n::DataModel`.
|
|
49
|
+
|
|
50
|
+
## Output invariants
|
|
51
|
+
|
|
52
|
+
- UTF-8 encoded, no BOM
|
|
53
|
+
- NFC characters preserved
|
|
54
|
+
- Document-order traversal
|
|
55
|
+
- Entity references: `&` → `&`, `<` → `<`, `>` → `>`
|
|
56
|
+
- Attribute values: also escape `"`, tab, LF, CR
|
|
57
|
+
- Empty elements expanded: `<foo/>` → `<foo></foo>`
|
|
58
|
+
- Line endings normalized to LF (the parser already does this)
|
|
59
|
+
|
|
60
|
+
## Cross-verification
|
|
61
|
+
|
|
62
|
+
The libxmlsec1-produced fixtures in `spec/fixtures/xmldsig/` verify
|
|
63
|
+
byte-exact against our C14N output — the implementation matches
|
|
64
|
+
libxml2's C-based canonicalization for the cases the Ruby reference
|
|
65
|
+
exercises.
|
data/docs/c14n/api.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# C14N API
|
|
2
|
+
|
|
3
|
+
`node_or_xml` accepts a `Moxml::Node`, `Moxml::Document`, or XML
|
|
4
|
+
`String` on every entry point.
|
|
5
|
+
|
|
6
|
+
## Convenience methods
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Inclusive C14N 1.0
|
|
10
|
+
Moxml::C14n.canonicalize(node_or_xml, with_comments: false)
|
|
11
|
+
|
|
12
|
+
# Exclusive C14N 1.0
|
|
13
|
+
Moxml::C14n.canonicalize_exclusive(
|
|
14
|
+
node_or_xml,
|
|
15
|
+
with_comments: false,
|
|
16
|
+
inclusive_namespaces: [], # InclusiveNamespacesPrefixList parameter
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Inclusive C14N 1.1
|
|
20
|
+
Moxml::C14n.canonicalize_inclusive11(node_or_xml, with_comments: false)
|
|
21
|
+
|
|
22
|
+
# Node-set subset canonicalization: only nodes matched by the XPath
|
|
23
|
+
# render (position-path matching — see docs on enveloped signatures)
|
|
24
|
+
Moxml::C14n.canonicalize_subset(node_or_xml, "//signed",
|
|
25
|
+
with_comments: false)
|
|
26
|
+
|
|
27
|
+
# Canonical comparison: do two inputs share one canonical form?
|
|
28
|
+
# (inclusive C14N 1.0, comments dropped, on both sides)
|
|
29
|
+
Moxml::C14n.same_canonical_form?(left, right) # nodes or raw strings
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
All canonicalize methods return canonical UTF-8 octet strings.
|
|
33
|
+
|
|
34
|
+
## Direct engine access
|
|
35
|
+
|
|
36
|
+
Used by the signature algorithms:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Moxml::C14n::Inclusive10.new.canonicalize(node, with_comments:,
|
|
40
|
+
inclusive_namespaces:)
|
|
41
|
+
Moxml::C14n::Inclusive11.new.canonicalize(node, with_comments:,
|
|
42
|
+
inclusive_namespaces:)
|
|
43
|
+
Moxml::C14n::Exclusive.new.canonicalize(node, with_comments:,
|
|
44
|
+
inclusive_namespaces:)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Digests
|
|
48
|
+
|
|
49
|
+
`Node#digest` (engine-delegated where available) computes the canonical
|
|
50
|
+
digest of a subtree:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
node.digest # SHA-256 over inclusive C14N 1.0
|
|
54
|
+
node.digest(drop_ws_text: true)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Backward-compat escape helpers
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
Moxml::C14n.escape_text("a & b < c > d")
|
|
61
|
+
Moxml::C14n.escape_attribute(%(a"b\tc\nd))
|
|
62
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# C14N examples
|
|
2
|
+
|
|
3
|
+
## Whole-document canonicalization
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
doc = Moxml.new.parse(xml)
|
|
7
|
+
Moxml::C14n.canonicalize(doc)
|
|
8
|
+
# => "<?xml version=\"1.0\"?>\n<a b=\"1\">text</a>\n"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Subset canonicalization (enveloped signature shape)
|
|
12
|
+
|
|
13
|
+
Only nodes matched by the XPath expression render — the same-document
|
|
14
|
+
reference pattern:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
Moxml::C14n.canonicalize_subset(doc, "//ds:SignedInfo",
|
|
18
|
+
{ "ds" => "http://www.w3.org/2000/09/xmldsig#" })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Canonical comparison
|
|
22
|
+
|
|
23
|
+
Testing two documents for semantic equality — whitespace, attribute
|
|
24
|
+
order, and prefix choice are eliminated before comparison:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
Moxml::C14n.same_canonical_form?(v1_document, v2_document) # => true
|
|
28
|
+
Moxml::C14n.same_canonical_form?(fixture_xml, output_string) # => true
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Cross-adapter verification
|
|
32
|
+
|
|
33
|
+
A signature produced with one adapter must verify with another — the
|
|
34
|
+
canonical bytes are byte-identical across adapters:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
%i[leptris nokogiri rexml ox libxml].map do |adapter|
|
|
38
|
+
Moxml.new(adapter).parse(xml).then { |d| Moxml::C14n.canonicalize(d) }
|
|
39
|
+
end.uniq.size # => 1
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Digest of a subtree
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
signed_element.digest
|
|
46
|
+
# engine-delegated SHA-256 over the subtree's canonical form
|
|
47
|
+
```
|
data/docs/signature/c14n.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Canonicalization (C14N)
|
|
1
|
+
# Canonicalization (C14N) in XML signature
|
|
2
2
|
|
|
3
3
|
Canonicalization is the load-bearing primitive for XML signature.
|
|
4
4
|
Two documents that differ only in surface representation (whitespace,
|
|
@@ -6,7 +6,9 @@ attribute order, namespace prefix choice) must produce identical
|
|
|
6
6
|
canonical bytes — otherwise signatures won't verify.
|
|
7
7
|
|
|
8
8
|
`Moxml::C14n` is a top-level moxml feature, sibling to `Moxml::XPath`,
|
|
9
|
-
`Moxml::Builder`, and `Moxml::SAX`.
|
|
9
|
+
`Moxml::Builder`, and `Moxml::SAX`. The full C14N documentation tree
|
|
10
|
+
lives at [docs/c14n/](../c14n/README.md) — this page covers the
|
|
11
|
+
signature-specific view.
|
|
10
12
|
|
|
11
13
|
## Algorithms
|
|
12
14
|
|
|
@@ -97,10 +99,10 @@ and it's much faster than pure Ruby. But:
|
|
|
97
99
|
3. **Canon already had the mature pure-Ruby implementation.** Porting it
|
|
98
100
|
was less work than building a hybrid adapter-delegated system.
|
|
99
101
|
|
|
100
|
-
**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
**Shipped:** optional adapter-level C14N delegation — the Nokogiri
|
|
103
|
+
(libxml2) and leptris (engine) adapters expose native canonicalization
|
|
104
|
+
behind a lazy byte-safety probe, byte-identical with the pure-Ruby
|
|
105
|
+
path (see [docs/c14n/algorithms.md](../c14n/algorithms.md#adapter-delegation)).
|
|
104
106
|
|
|
105
107
|
## Cross-verification
|
|
106
108
|
|
|
@@ -725,7 +725,16 @@ module Moxml
|
|
|
725
725
|
# exactly the plan's shape, with no wrapper minting at all.
|
|
726
726
|
# Marker-bearing documents stay on the generic path (the bulk
|
|
727
727
|
# stream has no marker split).
|
|
728
|
-
|
|
728
|
+
# moxml#344: engines 1.9.314-1.9.319 mis-match on the plan
|
|
729
|
+
# path — child-row exact-URI ns_uri pointed into build
|
|
730
|
+
# buffers (rows vanish after GC, leptris#1585) and plain
|
|
731
|
+
# attribute rows did not leniently match namespace-qualified
|
|
732
|
+
# wire attributes (nil captures, leptris#1586). Both are
|
|
733
|
+
# SILENT wrong answers, so the gate names the fixed engine:
|
|
734
|
+
# below 1.9.320.0 the plan faces degrade to the
|
|
735
|
+
# always-correct generic wrapper walk.
|
|
736
|
+
NATIVE_PLAN_ROWS =
|
|
737
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.320.0")
|
|
729
738
|
|
|
730
739
|
# leptris-ruby#272 (binding 1.9.201.1): Native.plan_structs —
|
|
731
740
|
# the C struct executor behind Moxml::StructPlan. The floor
|
|
@@ -735,9 +744,12 @@ module Moxml
|
|
|
735
744
|
# below the fix. The probe guards the version gate: a
|
|
736
745
|
# numerically newer lockstep can publish without the face
|
|
737
746
|
# (1.9.199.0 shipped without it), and the gate alone would
|
|
738
|
-
# NoMethodError.
|
|
747
|
+
# NoMethodError. Floor also carries moxml#344: 1.9.314-1.9.319
|
|
748
|
+
# mis-match on the plan path (leptris#1585/#1586, silent nil
|
|
749
|
+
# captures) — the struct executor degrades to the Ruby Plan
|
|
750
|
+
# fallback below the fix, same as the varargs-mint floor.
|
|
739
751
|
NATIVE_PLAN_STRUCTS =
|
|
740
|
-
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.
|
|
752
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.320.0") &&
|
|
741
753
|
(!defined?(::Leptris::XML::Native) ||
|
|
742
754
|
::Leptris::XML::Native.respond_to?(:plan_structs))
|
|
743
755
|
|
|
@@ -781,7 +793,7 @@ module Moxml
|
|
|
781
793
|
end
|
|
782
794
|
|
|
783
795
|
def self.plan_rows(native)
|
|
784
|
-
return nil unless NATIVE_READ_LAYER
|
|
796
|
+
return nil unless NATIVE_READ_LAYER && NATIVE_PLAN_ROWS
|
|
785
797
|
|
|
786
798
|
doc = if native.is_a?(::Leptris::XML::Document)
|
|
787
799
|
native
|
data/lib/moxml/version.rb
CHANGED
|
@@ -77,4 +77,28 @@ RSpec.describe "Moxml::Plan adapter conformance" do
|
|
|
77
77
|
)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
|
+
|
|
81
|
+
describe "plan-gate floors (moxml#344)" do
|
|
82
|
+
let(:gate_ctx) { Moxml.new(:leptris) }
|
|
83
|
+
|
|
84
|
+
it "arms both plan faces on engines carrying the #1585/#1586 fixes" do
|
|
85
|
+
skip "testing engine predates the floor" unless
|
|
86
|
+
Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.320.0")
|
|
87
|
+
|
|
88
|
+
expect(Moxml::Adapter::Leptris::NATIVE_PLAN_ROWS).to be(true)
|
|
89
|
+
expect(Moxml::Adapter::Leptris::NATIVE_PLAN_STRUCTS).to be(true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "captures namespace-qualified wire attributes (leptris#1586)" do
|
|
93
|
+
skip "plan faces degraded below 1.9.320 (moxml#344)" unless
|
|
94
|
+
Moxml::Adapter::Leptris::NATIVE_PLAN_ROWS
|
|
95
|
+
|
|
96
|
+
plan = Moxml::Plan.new do
|
|
97
|
+
on("e") { |attrs| @captured = attrs }
|
|
98
|
+
end
|
|
99
|
+
plan.parse('<r xmlns:w="urn:w"><e w:name="Aptos"/></r>', gate_ctx)
|
|
100
|
+
captured = plan.instance_variable_get(:@captured)
|
|
101
|
+
expect(captured).to include("w:name" => "Aptos")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
80
104
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.118
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -89,6 +89,10 @@ files:
|
|
|
89
89
|
- docs/_tutorials/index.adoc
|
|
90
90
|
- docs/_tutorials/namespace-handling.adoc
|
|
91
91
|
- docs/_tutorials/xpath-queries.adoc
|
|
92
|
+
- docs/c14n/README.md
|
|
93
|
+
- docs/c14n/algorithms.md
|
|
94
|
+
- docs/c14n/api.md
|
|
95
|
+
- docs/c14n/examples.md
|
|
92
96
|
- docs/index.adoc
|
|
93
97
|
- docs/signature/algorithms.md
|
|
94
98
|
- docs/signature/architecture.md
|