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,148 @@
1
+ # Moxml::Signature — Architecture
2
+
3
+ ## Where it lives
4
+
5
+ `Moxml::Signature` is a sub-module of moxml that implements W3C XML
6
+ Signature (xmldsig-core-1.1). It is **XML implementation agnostic** —
7
+ every XML operation flows through `Moxml::Document` / `Moxml::Element`,
8
+ so the same signature code works whether you parse with Nokogiri, Oga,
9
+ REXML, Ox, or LibXML.
10
+
11
+ C14N itself is a top-level `Moxml::C14n` feature, sibling to
12
+ `Moxml::XPath`, `Moxml::Builder`, and `Moxml::SAX`. Signature uses it;
13
+ so can any other consumer (e.g., the sibling `canon` gem).
14
+
15
+ ## Module layout
16
+
17
+ ```
18
+ lib/moxml.rb # top-level, autoloads Signature and C14n
19
+ lib/moxml/signature.rb # Signature namespace + .sign/.verify entry points
20
+
21
+ lib/moxml/signature/errors.rb # error hierarchy
22
+ lib/moxml/signature/algorithms.rb # OCP registry hub
23
+ lib/moxml/signature/algorithms/ # concrete algorithms (digests, sig methods, transforms)
24
+ lib/moxml/signature/model/ # PORO models
25
+ lib/moxml/signature/serializer.rb # model → XML (uses moxml primitives)
26
+ lib/moxml/signature/parser.rb # XML → model
27
+ lib/moxml/signature/reference_resolver.rb # Reference URI → node-set / octets
28
+ lib/moxml/signature/transform_pipeline.rb # shared transform chain (DRY)
29
+ lib/moxml/signature/signer.rb # spec §3.1 signing flow
30
+ lib/moxml/signature/verifier.rb # spec §3.2 verification flow
31
+ lib/moxml/signature/key_extractor.rb # X509 / RSA / DSA / EC / KeyName → OpenSSL key
32
+ lib/moxml/signature/verification_result.rb
33
+ lib/moxml/signature/single_verification_result.rb
34
+ lib/moxml/signature/reference_result.rb
35
+
36
+ lib/moxml/c14n.rb # top-level C14n namespace
37
+ lib/moxml/c14n/ # canon-ported engine + moxml-native Exclusive
38
+ ```
39
+
40
+ ## Layering
41
+
42
+ ```
43
+ ┌──────────────────────────────────────────────────────────────────┐
44
+ │ Application code │
45
+ │ Moxml::Signature.sign / .verify │
46
+ └──────────────────────────────────────────────────────────────────┘
47
+
48
+ ┌──────────────────────────────────────────────────────────────────┐
49
+ │ Orchestration: Signer, Verifier, TransformPipeline, KeyExtractor│
50
+ └──────────────────────────────────────────────────────────────────┘
51
+
52
+ ┌──────────────────────┐ ┌──────────────────────────────────────┐
53
+ │ Algorithms (OCP hub) │ │ Models: Signature, SignedInfo, etc. │
54
+ └──────────────────────┘ └──────────────────────────────────────┘
55
+
56
+ ┌──────────────────────────────────────────────────────────────────┐
57
+ │ Moxml::C14n (canon-ported Inclusive + moxml-native Exclusive) │
58
+ └──────────────────────────────────────────────────────────────────┘
59
+
60
+ ┌──────────────────────────────────────────────────────────────────┐
61
+ │ Moxml::Document / Element / Text / Namespace / Attribute │
62
+ │ (adapter-agnostic — Nokogiri, Oga, REXML, Ox, LibXML) │
63
+ └──────────────────────────────────────────────────────────────────┘
64
+
65
+ OpenSSL (crypto)
66
+ ```
67
+
68
+ ## Core design decisions
69
+
70
+ ### 1. Algorithm registry as the OCP hub
71
+
72
+ Every W3C algorithm (digest, signature method, canonicalization,
73
+ transform) is identified by URI. The `Moxml::Signature::Algorithms`
74
+ module is the registry; adding a new algorithm means:
75
+
76
+ 1. Subclass the relevant base (`DigestBase`, `SignatureMethodBase`,
77
+ `CanonicalizationBase`, `TransformBase`).
78
+ 2. Declare `identifier "http://..."` on the subclass.
79
+ 3. Add an autoload entry in `algorithms.rb` and a reference in
80
+ `load_builtins!`.
81
+
82
+ No edits to existing code. The registry has four categories:
83
+ `:digest`, `:signature_method`, `:canonicalization`, `:transform`.
84
+
85
+ ### 2. Models are POROs; serialization is a service
86
+
87
+ Models (`Model::Signature`, `Model::SignedInfo`, `Model::Reference`,
88
+ etc.) are plain Ruby objects with `attr_accessor`. They do **not** own
89
+ their wire shape. A dedicated `Serializer` translates model → XML using
90
+ moxml primitives; `Parser` translates XML → model. This keeps the data
91
+ shape and the wire shape independent, and matches the user's global
92
+ rule ("no hand-rolled serialization on model classes").
93
+
94
+ ### 3. Signer / Verifier orchestrate, don't compute
95
+
96
+ `Signer` walks the references, delegates to the transform pipeline,
97
+ computes digests via `DigestMethod` instances, and signs the
98
+ canonicalized SignedInfo. It contains no algorithm-specific logic.
99
+
100
+ `Verifier` follows Best Practice 1: authenticate SignatureValue first,
101
+ then run reference transforms. Errors are captured into the result
102
+ object (`SingleVerificationResult#error`), not raised, so a malicious
103
+ signature cannot panic the application.
104
+
105
+ ### 4. TransformPipeline is shared by Signer and Verifier
106
+
107
+ The transform-chain logic (lookup algorithm, coerce input type, apply,
108
+ repeat) is the same for signing and verifying. It lives in
109
+ `TransformPipeline` — DRY.
110
+
111
+ ### 5. C14N is shared infrastructure
112
+
113
+ Inclusive C14N is ported from `~/src/lutaml/canon` (mature, ~1,200
114
+ lines, full node-set subset support, xml:base fixup, xml:* inheritable
115
+ attribute resolution). Exclusive C14N is moxml-native (canon doesn't
116
+ implement it). Both expose the same `#canonicalize(node, with_comments:,
117
+ inclusive_namespaces:)` interface.
118
+
119
+ ## Adapter-agnostic invariant
120
+
121
+ Every XML operation — parse, walk, serialize, canonicalize — goes
122
+ through `Moxml::Node`. The signature module never imports Nokogiri,
123
+ Oga, REXML, Ox, or LibXML directly. Switching adapters does not change
124
+ signature behavior.
125
+
126
+ The one exception is the `context:` parameter threaded through every
127
+ constructor. When a transform receives octet-stream input, it parses
128
+ with the same adapter the caller used (`context.parse(...)`), preserving
129
+ byte-exact canonicalization across adapters.
130
+
131
+ ## Cross-verification
132
+
133
+ `spec/fixtures/xmldsig/sign2-result.xml` and `sign3-result.xml` are
134
+ real libxmlsec1-produced signatures (from the Ruby
135
+ `nokogiri-xmlsec-instructure` reference). Both verify byte-exact against
136
+ `Moxml::Signature.verify`, proving the C14N and signing logic matches a
137
+ battle-tested C implementation.
138
+
139
+ ## What this module deliberately doesn't do
140
+
141
+ - **XPath Filter transform** — Best Practice 5 says avoid. The Enveloped
142
+ Signature transform walks ancestors directly, no XPath needed.
143
+ - **XSLT transform** — Best Practice 3 says avoid. Disabled.
144
+ - **External URI dereferencing** — Best Practice 8 says constrain.
145
+ Applications must provide their own resolver.
146
+ - **X.509 chain validation** — application responsibility (trust policy).
147
+ - **XML Encryption** — separate spec (xmlenc-core-1.1).
148
+ - **XAdES** — separate spec (ETSI TS 101 903).
@@ -0,0 +1,110 @@
1
+ # Canonicalization (C14N)
2
+
3
+ Canonicalization is the load-bearing primitive for XML signature.
4
+ Two documents that differ only in surface representation (whitespace,
5
+ attribute order, namespace prefix choice) must produce identical
6
+ canonical bytes — otherwise signatures won't verify.
7
+
8
+ `Moxml::C14n` is a top-level moxml feature, sibling to `Moxml::XPath`,
9
+ `Moxml::Builder`, and `Moxml::SAX`.
10
+
11
+ ## Algorithms
12
+
13
+ ### Inclusive C14N 1.0 / 1.1
14
+
15
+ - W3C: <https://www.w3.org/TR/xml-c14n/>, <https://www.w3.org/TR/xml-c14n11/>
16
+ - Implementation: **ported from `~/src/lutaml/canon`** (~1,200 lines)
17
+ - Files: `lib/moxml/c14n/inclusive_10.rb`, `inclusive_11.rb`,
18
+ `data_model.rb`, `processor.rb`, `namespace_handler.rb`,
19
+ `attribute_handler.rb`, `xml_base_handler.rb`, `character_encoder.rb`,
20
+ `node.rb`, `nodes/*.rb`
21
+ - "Attracts" ancestor context: at the apex, every in-scope namespace is
22
+ rendered, including those inherited from outside the canonicalization
23
+ subset.
24
+
25
+ ### Exclusive C14N 1.0
26
+
27
+ - W3C: <https://www.w3.org/TR/xml-exc-c14n/>
28
+ - Implementation: **moxml-native** (canon does not implement exclusive)
29
+ - Files: `lib/moxml/c14n/exclusive.rb`, `writer.rb`, `namespace_context.rb`
30
+ - "Repels" ancestor context: only namespaces visibly used by the apex
31
+ element's qualified name or attributes are rendered. Keeps signatures
32
+ valid when subdocuments are moved between XML contexts (e.g., into a
33
+ SOAP envelope).
34
+
35
+ ## Public API
36
+
37
+ ```ruby
38
+ # Convenience: inclusive C14N 1.0
39
+ Moxml::C14n.canonicalize(node_or_xml, with_comments: false)
40
+ # → canonical UTF-8 octet String
41
+
42
+ # Convenience: exclusive C14N 1.0
43
+ Moxml::C14n.canonicalize_exclusive(
44
+ node_or_xml,
45
+ with_comments: false,
46
+ inclusive_namespaces: [], # InclusiveNamespacesPrefixList parameter
47
+ )
48
+ # → canonical UTF-8 octet String
49
+
50
+ # Direct engine access (used by signature algorithms)
51
+ Moxml::C14n::Inclusive10.new.canonicalize(node, with_comments:, inclusive_namespaces:)
52
+ Moxml::C14n::Inclusive11.new.canonicalize(node, with_comments:, inclusive_namespaces:)
53
+ Moxml::C14n::Exclusive.new.canonicalize(node, with_comments:, inclusive_namespaces:)
54
+ ```
55
+
56
+ `node_or_xml` accepts a `Moxml::Node`, `Moxml::Document`, or XML `String`.
57
+
58
+ ## Data model
59
+
60
+ The canon-derived inclusive C14N walks an intermediate data model
61
+ (`Moxml::C14n::Nodes::*`) rather than the live `Moxml::Node` tree. This
62
+ is because canonicalization needs:
63
+
64
+ - **Node-set membership flags** for subset canonicalization (spec §3).
65
+ Same-document references select a node-set; only selected nodes are
66
+ rendered.
67
+ - **Sorted namespace and attribute axes** per spec §2.3 / §2.4.
68
+ - **xml:base fixup** per RFC 3986 with C14N 1.1 modifications.
69
+ - **xml:* inheritable attribute resolution** (xml:lang, xml:space) from
70
+ omitted ancestors.
71
+
72
+ The data model is built from `Moxml::Node` via `Moxml::C14n::DataModel`.
73
+
74
+ ## Output invariants
75
+
76
+ All algorithms produce canonical octets with these properties:
77
+
78
+ - UTF-8 encoded, no BOM
79
+ - NFC characters preserved
80
+ - Document-order traversal
81
+ - Entity references: `&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;`
82
+ - Attribute values: also escape `"`, tab, LF, CR
83
+ - Empty elements expanded: `<foo/>` → `<foo></foo>`
84
+ - Line endings normalized to LF (XML parser already does this)
85
+
86
+ ## Why not delegate to Nokogiri's native C14N?
87
+
88
+ Nokogiri (via libxml2) has both inclusive and exclusive C14N built-in,
89
+ and it's much faster than pure Ruby. But:
90
+
91
+ 1. **XML-agnosticity.** Moxml's whole point is that switching adapters
92
+ doesn't change behavior. If C14N were Nokogiri-only, Oga/REXML/Ox
93
+ users would get no canonicalization.
94
+ 2. **Adapter-coupled canonicalization breaks cross-adapter signature
95
+ verification.** A signature produced with Nokogiri must verify with
96
+ Oga — same canonical bytes.
97
+ 3. **Canon already had the mature pure-Ruby implementation.** Porting it
98
+ was less work than building a hybrid adapter-delegated system.
99
+
100
+ **Future work:** add an optional adapter-level C14N delegation. The
101
+ Nokogiri adapter could expose `canonicalize(node, ...)` and the
102
+ `Moxml::C14n` top-level method would delegate when available. This is
103
+ documented in TODO.complete/19.
104
+
105
+ ## Cross-verification
106
+
107
+ The libxmlsec1-produced fixtures in `spec/fixtures/xmldsig/` verify
108
+ byte-exact against our C14N output. This proves the implementation
109
+ matches libxml2's C-based canonicalization for the cases the Ruby
110
+ reference exercises.
@@ -0,0 +1,109 @@
1
+ # Examples
2
+
3
+ Runnable examples live in `examples/signature/`. Run with:
4
+
5
+ ```bash
6
+ bundle exec ruby examples/signature/enveloped_rsa.rb
7
+ bundle exec ruby examples/signature/hmac.rb
8
+ bundle exec ruby examples/signature/auto_key_extraction.rb
9
+ ```
10
+
11
+ ## Minimal enveloped RSA-SHA256 signature
12
+
13
+ ```ruby
14
+ require "moxml"
15
+ require "moxml/signature"
16
+ require "openssl"
17
+
18
+ ctx = Moxml.new(:nokogiri)
19
+ key = OpenSSL::PKey::RSA.generate(2048)
20
+
21
+ doc = ctx.parse("<doc><greeting>Hello, World!</greeting></doc>")
22
+
23
+ # Sign — produces a Model::Signature
24
+ signature = Moxml::Signature.sign(
25
+ context: ctx,
26
+ document: doc,
27
+ key: key,
28
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
29
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
30
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
31
+ reference_uri: "",
32
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
33
+ )
34
+
35
+ # Serialize the signature into XML and attach to the document
36
+ serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
37
+ doc.root.add_child(serialized.root)
38
+
39
+ puts doc.to_xml
40
+ ```
41
+
42
+ ## Verify with the public key
43
+
44
+ ```ruby
45
+ result = Moxml::Signature.verify(
46
+ context: ctx,
47
+ document: doc,
48
+ key: key, # private key works; public key alone is enough
49
+ )
50
+
51
+ puts "Valid: #{result.valid?}"
52
+ puts "Signature count: #{result.signature_count}"
53
+ result.results.each do |r|
54
+ puts " signature_valid=#{r.signature_valid?}"
55
+ r.references.each { |ref| puts " ref #{ref.uri.inspect}: #{ref.valid?}" }
56
+ end
57
+ ```
58
+
59
+ ## Auto-extract key from X509Certificate
60
+
61
+ ```ruby
62
+ # sign3-result.xml embeds the signing certificate in KeyInfo.
63
+ # No explicit key is needed — the Verifier extracts it automatically.
64
+ doc = ctx.parse(File.read("spec/fixtures/xmldsig/sign3-result.xml"))
65
+ result = Moxml::Signature.verify(context: ctx, document: doc)
66
+ puts "Auto-extracted: #{result.valid?}"
67
+ ```
68
+
69
+ ## KeyName-based key resolution
70
+
71
+ ```ruby
72
+ # Signer used <KeyName>my-key</KeyName>; verifier resolves via key_map:
73
+ result = Moxml::Signature.verify(
74
+ context: ctx,
75
+ document: doc,
76
+ key_map: { "my-key" => trusted_public_key },
77
+ )
78
+ ```
79
+
80
+ ## HMAC with truncation
81
+
82
+ ```ruby
83
+ signature = Moxml::Signature.sign(
84
+ context: ctx,
85
+ document: doc,
86
+ key: "shared-secret",
87
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
88
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
89
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
90
+ reference_uri: "",
91
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
92
+ )
93
+ ```
94
+
95
+ HMAC `OutputLength` truncation below `max(hash_bits/2, 80)` is rejected.
96
+
97
+ ## Adding a custom algorithm
98
+
99
+ ```ruby
100
+ class MyDigest < Moxml::Signature::Algorithms::DigestBase
101
+ identifier "http://example.com/my-digest"
102
+
103
+ def compute_digest(data)
104
+ OpenSSL::Digest.digest("SHA3-256", data)
105
+ end
106
+ end
107
+
108
+ # Now usable in any Reference#digest_method
109
+ ```
@@ -0,0 +1,132 @@
1
+ # Signing and verification flows
2
+
3
+ ## Signer (spec §3.1)
4
+
5
+ ```
6
+ Moxml::Signature.sign(context:, document:, key:, **options)
7
+
8
+
9
+ build a Model::Signature with the requested algorithms
10
+
11
+
12
+ Signer#sign
13
+
14
+ ├── for each Reference in SignedInfo:
15
+ │ ├── ReferenceResolver.resolve(uri) → node or octets
16
+ │ ├── TransformPipeline.apply(input, transforms)
17
+ │ ├── TransformPipeline.to_octets(transformed, reference)
18
+ │ └── DigestMethod#digest_base64(canonical) → Reference#digest_value
19
+
20
+ ├── Serializer.serialize_signed_info(signed_info) → Moxml::Document
21
+ ├── CanonicalizationMethod#canonicalize(signed_info_root) → octets
22
+ └── SignatureMethod#sign(canonical_octets, key) → SignatureValue
23
+ ```
24
+
25
+ The Signer never touches algorithms directly — every step delegates to
26
+ the algorithm registry. This is OCP: new algorithms are added without
27
+ touching the Signer.
28
+
29
+ ## Verifier (spec §3.2 + Best Practice 1)
30
+
31
+ ```
32
+ Moxml::Signature.verify(context:, document:, key: nil, key_map: {})
33
+
34
+
35
+ find all ds:Signature elements in the document
36
+
37
+ ▼ for each Signature
38
+ verify_one(signature_element)
39
+
40
+ ├── Parser.parse(signature_element) → Model::Signature
41
+
42
+ ├── verify_signature_value FIRST (Best Practice 1)
43
+ │ ├── find original SignedInfo element (do NOT re-serialize)
44
+ │ ├── CanonicalizationMethod#canonicalize(signed_info_elem)
45
+ │ ├── resolve_key(signature) (KeyExtractor if no key:)
46
+ │ └── SignatureMethod#verify(canonical, key, sig_value)
47
+
48
+ └── if signature value verifies:
49
+ for each Reference:
50
+ ├── ReferenceResolver.resolve(uri)
51
+ ├── TransformPipeline.apply(...)
52
+ ├── TransformPipeline.to_octets(...)
53
+ └── DigestMethod#digest_base64 vs Reference#digest_value
54
+ ```
55
+
56
+ ### Best Practice 1: authenticate before transforms
57
+
58
+ The verifier deliberately runs `SignatureValue` validation BEFORE
59
+ applying any transforms. Reason: a malicious signature could include
60
+ XSLT or expensive XPath transforms. Best Practice 1 says only run
61
+ those after authenticating the signer.
62
+
63
+ If `SignatureValue` doesn't verify, we skip reference transforms
64
+ entirely. The result object reports `signature_valid?: false` and
65
+ `references: []`.
66
+
67
+ ### Why we canonicalize the original element
68
+
69
+ The verifier canonicalizes the SignedInfo **as it appears in the
70
+ document**, not a re-serialized model. Reason: re-serialization could
71
+ change namespace prefixes (e.g., default-namespace SignedInfo might
72
+ become `ds:SignedInfo`), breaking the byte-exact match the signature
73
+ was computed against.
74
+
75
+ The Signer serializes (it has to — it's building new XML). The Verifier
76
+ has the original element from the document and uses it directly. This
77
+ asymmetry is intentional and necessary for cross-verification with
78
+ libxmlsec1-produced signatures.
79
+
80
+ ### Errors are captured, not raised
81
+
82
+ `SingleVerificationResult#error` carries any `VerificationError` or
83
+ `UnknownAlgorithm` that caused failure. The verifier returns a result
84
+ object rather than raising, so a hostile signature cannot panic the
85
+ application. Use `result.results.first.error` for debugging.
86
+
87
+ ## Key resolution
88
+
89
+ ```ruby
90
+ Moxml::Signature.verify(context:, document:) # no key:
91
+ ```
92
+
93
+ When no `key:` is passed, the Verifier uses `KeyExtractor` to derive
94
+ one from the signature's `KeyInfo`:
95
+
96
+ 1. **X509Certificate** (preferred) — decode base64 DER →
97
+ `OpenSSL::X509::Certificate` → `.public_key`
98
+ 2. **RSAKeyValue** — reconstruct via ASN.1 (OpenSSL 3.x dropped
99
+ `RSA.new(n, e)`)
100
+ 3. **DSAKeyValue** — reconstruct via ASN.1
101
+ 4. **ECKeyValue** — build SubjectPublicKeyInfo DER for the named curve
102
+ (OpenSSL 3.x made PKey immutable)
103
+ 6. **KeyName** — look up in the application-supplied `key_map:`
104
+
105
+ OpenSSL 3.x compatibility notes are inline in `key_extractor.rb`.
106
+
107
+ ## Transform pipeline
108
+
109
+ `TransformPipeline` is shared by Signer and Verifier. Each Transform
110
+ in the chain declares `input_type` (`:octets` or `:nodeset`) and
111
+ `output_type`; the pipeline coerces types between transforms per
112
+ spec §4.4.3.2:
113
+
114
+ - `octets → nodeset`: parse as XML via the caller's `context:`
115
+ - `nodeset → octets`: apply inclusive C14N 1.0
116
+
117
+ Final conversion to octets (for digesting) uses the last canonicalization
118
+ transform in the chain if any, else inclusive C14N 1.0 as the spec default.
119
+
120
+ ## Result objects
121
+
122
+ ```ruby
123
+ result = Moxml::Signature.verify(context:, document:, key:)
124
+ result.valid? # → bool (all signatures valid)
125
+ result.signature_count # → Integer
126
+ result.results # → [SingleVerificationResult, ...]
127
+ result.results.first.signature_valid?
128
+ result.results.first.references # → [ReferenceResult, ...]
129
+ result.results.first.references.first.valid?
130
+ result.results.first.error # → Exception or nil
131
+ result.failing # → [SingleVerificationResult, ...]
132
+ ```
@@ -0,0 +1,115 @@
1
+ # Quick reference
2
+
3
+ ## Public API
4
+
5
+ ```ruby
6
+ # Sign a document
7
+ signature = Moxml::Signature.sign(
8
+ context:, # Moxml::Context (from Moxml.new(:nokogiri))
9
+ document:, # Moxml::Document to sign
10
+ key:, # OpenSSL::PKey::* or HMAC secret String
11
+ signature_method:, # algorithm URI
12
+ canonicalization_method:, # algorithm URI
13
+ digest_method:, # algorithm URI
14
+ reference_uri:, # "" for whole document
15
+ transforms:, # array of algorithm URIs
16
+ key_info: nil, # optional Model::KeyInfo
17
+ signature_id: nil, # optional Id attribute
18
+ )
19
+
20
+ # Verify a document
21
+ result = Moxml::Signature.verify(
22
+ context:,
23
+ document:,
24
+ key: nil, # optional; auto-extracted from KeyInfo if absent
25
+ key_map: {}, # for KeyName-based resolution
26
+ )
27
+ ```
28
+
29
+ ## Algorithm URIs
30
+
31
+ ### Digests
32
+ - `http://www.w3.org/2000/09/xmldsig#sha1` (verification only)
33
+ - `http://www.w3.org/2001/04/xmlenc#sha256` (REQUIRED)
34
+ - `http://www.w3.org/2001/04/xmlenc#sha512`
35
+ - `http://www.w3.org/2001/04/xmldsig-more#sha224`
36
+ - `http://www.w3.org/2001/04/xmldsig-more#sha384`
37
+
38
+ ### Signature methods
39
+ - RSA-PKCS1v1.5: `http://www.w3.org/2001/04/xmldsig-more#rsa-sha{1,224,256,384,512}`
40
+ - HMAC: `http://www.w3.org/2001/04/xmldsig-more#hmac-sha{1,224,256,384,512}`
41
+ - ECDSA: `http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha{1,224,256,384,512}`
42
+ - DSA: `http://www.w3.org/2000/09/xmldsig#dsa-sha1`, `http://www.w3.org/2009/xmldsig11#dsa-sha256`
43
+
44
+ ### Canonicalization
45
+ - Exclusive: `http://www.w3.org/2001/10/xml-exc-c14n#` (add `#WithComments`)
46
+ - Inclusive 1.0: `http://www.w3.org/TR/2001/REC-xml-c14n-20010315`
47
+ - Inclusive 1.1: `http://www.w3.org/2006/12/xml-c14n11`
48
+
49
+ ### Transforms
50
+ - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
51
+ - `http://www.w3.org/2000/09/xmldsig#base64`
52
+
53
+ ## Common algorithm combinations
54
+
55
+ ### Enveloped RSA-SHA256 (most common)
56
+ ```ruby
57
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
58
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
59
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
60
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
61
+ ```
62
+
63
+ ### HMAC-SHA256
64
+ ```ruby
65
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
66
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
67
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
68
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
69
+ ```
70
+
71
+ ### ECDSA-SHA256 (P-256)
72
+ ```ruby
73
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
74
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
75
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
76
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
77
+ ```
78
+
79
+ ## Result inspection
80
+
81
+ ```ruby
82
+ result.valid? # → bool (all signatures)
83
+ result.signature_count # → Integer
84
+ result.failing # → [SingleVerificationResult]
85
+
86
+ result.results.first.signature_valid? # → bool (crypto verify)
87
+ result.results.first.references # → [ReferenceResult]
88
+ result.results.first.failing_references # → [ReferenceResult]
89
+ result.results.first.error # → Exception or nil (debugging)
90
+ ```
91
+
92
+ ## C14N top-level API
93
+
94
+ ```ruby
95
+ Moxml::C14n.canonicalize(node_or_xml, with_comments: false)
96
+ Moxml::C14n.canonicalize_exclusive(node_or_xml, with_comments: false,
97
+ inclusive_namespaces: [])
98
+ ```
99
+
100
+ ## Errors
101
+
102
+ ```ruby
103
+ Moxml::Signature::Error # base
104
+ Moxml::Signature::SignatureError # generic
105
+ Moxml::Signature::UnknownAlgorithm # URI not in registry
106
+ Moxml::Signature::DuplicateAlgorithm # URI registered twice
107
+ Moxml::Signature::SigningError # signing failed
108
+ Moxml::Signature::VerificationError # verification failed
109
+ Moxml::Signature::ReferenceDigestMismatch # digest mismatch
110
+ Moxml::Signature::SignatureValueMismatch # crypto verify failed
111
+ Moxml::Signature::TransformError # transform pipeline
112
+ Moxml::Signature::CanonicalizationError # C14N failure
113
+ Moxml::Signature::MalformedSignatureError # XML schema violation
114
+ Moxml::Signature::SignatureKeyError # wrong key type
115
+ ```