moxml 0.1.25 → 0.1.26

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/README.adoc +64 -0
  4. data/docs/signature/algorithms.md +108 -0
  5. data/docs/signature/architecture.md +148 -0
  6. data/docs/signature/c14n.md +110 -0
  7. data/docs/signature/examples.md +109 -0
  8. data/docs/signature/flows.md +132 -0
  9. data/docs/signature/quick-reference.md +115 -0
  10. data/docs/signature/security.md +143 -0
  11. data/examples/signature/auto_key_extraction.rb +24 -0
  12. data/examples/signature/enveloped_rsa.rb +45 -0
  13. data/examples/signature/hmac.rb +33 -0
  14. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  15. data/lib/moxml/c14n/character_encoder.rb +29 -0
  16. data/lib/moxml/c14n/data_model.rb +145 -0
  17. data/lib/moxml/c14n/exclusive.rb +188 -0
  18. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  19. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  20. data/lib/moxml/c14n/namespace_context.rb +68 -0
  21. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  22. data/lib/moxml/c14n/node.rb +42 -0
  23. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  24. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  25. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  26. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  27. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  28. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  29. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  30. data/lib/moxml/c14n/nodes.rb +17 -0
  31. data/lib/moxml/c14n/processor.rb +118 -0
  32. data/lib/moxml/c14n/writer.rb +121 -0
  33. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  34. data/lib/moxml/c14n.rb +96 -0
  35. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  36. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  37. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  38. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  39. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  40. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  41. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  42. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  43. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  44. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  45. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  46. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  47. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  48. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  49. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  50. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  51. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  52. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  53. data/lib/moxml/signature/algorithms.rb +113 -0
  54. data/lib/moxml/signature/errors.rb +99 -0
  55. data/lib/moxml/signature/key_extractor.rb +172 -0
  56. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  57. data/lib/moxml/signature/model/digest_method.rb +15 -0
  58. data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
  59. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  60. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  61. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  62. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  63. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  64. data/lib/moxml/signature/model/key_info.rb +21 -0
  65. data/lib/moxml/signature/model/key_value.rb +20 -0
  66. data/lib/moxml/signature/model/object_element.rb +18 -0
  67. data/lib/moxml/signature/model/reference.rb +22 -0
  68. data/lib/moxml/signature/model/signature.rb +21 -0
  69. data/lib/moxml/signature/model/signature_value.rb +16 -0
  70. data/lib/moxml/signature/model/signed_info.rb +20 -0
  71. data/lib/moxml/signature/model/transform.rb +16 -0
  72. data/lib/moxml/signature/model/transforms.rb +36 -0
  73. data/lib/moxml/signature/model.rb +35 -0
  74. data/lib/moxml/signature/parser.rb +270 -0
  75. data/lib/moxml/signature/reference_resolver.rb +79 -0
  76. data/lib/moxml/signature/reference_result.rb +22 -0
  77. data/lib/moxml/signature/serializer.rb +163 -0
  78. data/lib/moxml/signature/signer.rb +73 -0
  79. data/lib/moxml/signature/single_verification_result.rb +31 -0
  80. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  81. data/lib/moxml/signature/verification_result.rb +27 -0
  82. data/lib/moxml/signature/verifier.rb +129 -0
  83. data/lib/moxml/signature.rb +94 -0
  84. data/lib/moxml/version.rb +1 -1
  85. data/lib/moxml.rb +2 -0
  86. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  87. data/reference-docs/w3c-xmldsig-core.md +400 -0
  88. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  89. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  90. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  91. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  92. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  93. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  94. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  95. data/spec/moxml/c14n/api_spec.rb +117 -0
  96. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  97. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  98. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  99. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  100. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  101. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  102. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  103. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  104. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  105. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  106. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  107. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  108. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  109. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  110. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  111. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  112. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  113. data/spec/moxml/signature/model_spec.rb +54 -0
  114. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  115. metadata +111 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed30996109d8cfb8057f216677cba8ca3cd3856287c38b6a7edab3251ebb2064
4
- data.tar.gz: f93d7c13863ef9d437e40403e4a48434ed1e1c5530ea2424cdeff710740243a0
3
+ metadata.gz: d829252809a02c146a09b8458ec5d9a297e5a31e91e6b04b6b6eda27a31e30a9
4
+ data.tar.gz: '09018d566262901ac5fe3fd864cd80e52dfe05f449e5b9e03054624044cab1af'
5
5
  SHA512:
6
- metadata.gz: 9b09c144a268507bb02a16a4c6ba7caf6d1a2fa003718a6fa0960a8b68ba03a09584a66fb9475b1c23cd2d79071f1cc8c313200749f997d317aa90e61fba3811
7
- data.tar.gz: 6389858a232d82faad5f6bbfd22d4828801c7a6d45a55a2a0f08d28c44647a588b52d2106649d408777453b571815b40c191f48c374cc49b648c58ad2be1e835
6
+ metadata.gz: 56cc005125bd3953a3e40f2867ef76f4bd2ea4823e3363904ff20330e67d62740ae26c444b10d6706d66d4c7b0f38ba4805e957cfbf6cb4cdf413a6480eefd03
7
+ data.tar.gz: 58ec645d3a117ecd8ad275705fc03a345da7b52485b8119e8e0c0ebb12611c36dc01a12600b4871db050a23c7ac93d04b77632499307b94b5f7bb6000ccf366b
@@ -13,6 +13,14 @@ on:
13
13
  Next release version. Possible values: x.y.z, major, minor, patch or pre|rc|etc
14
14
  required: true
15
15
  default: 'skip'
16
+ gated:
17
+ description: |
18
+ Defer rubygems publish to a separate repository_dispatch (do-release) run.
19
+ - false (default): bump + tag + publish in one step.
20
+ - true: bump + tag only; publish when do-release is triggered.
21
+ required: false
22
+ default: false
23
+ type: boolean
16
24
  repository_dispatch:
17
25
  types: [ do-release ]
18
26
 
@@ -21,6 +29,7 @@ jobs:
21
29
  uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
22
30
  with:
23
31
  next_version: ${{ github.event.inputs.next_version }}
32
+ gated: ${{ inputs.gated == true }}
24
33
  secrets:
25
34
  rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
26
35
  pat_token: ${{ secrets.GITHUB_TOKEN }}
data/README.adoc CHANGED
@@ -559,6 +559,70 @@ ruby examples/api_client/api_client.rb
559
559
  See the link:examples/README.md[examples README] for complete documentation and
560
560
  learning paths.
561
561
 
562
+ == XML Signature (Moxml::Signature)
563
+
564
+ Moxml includes an XML-implementation-agnostic implementation of W3C XML
565
+ Signature (xmldsig-core-1.1). Sign and verify documents with any moxml
566
+ adapter; the canonicalization engine produces byte-exact output that
567
+ cross-verifies with libxmlsec1.
568
+
569
+ === Quick start
570
+
571
+ [source,ruby]
572
+ ----
573
+ require "moxml"
574
+ require "moxml/signature"
575
+ require "openssl"
576
+
577
+ ctx = Moxml.new(:nokogiri)
578
+ key = OpenSSL::PKey::RSA.generate(2048)
579
+ doc = ctx.parse("<doc><greeting>Hello, World!</greeting></doc>")
580
+
581
+ # Sign
582
+ signature = Moxml::Signature.sign(
583
+ context: ctx, document: doc, key: key,
584
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
585
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
586
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
587
+ reference_uri: "",
588
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
589
+ )
590
+ serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
591
+ doc.root.add_child(serialized.root)
592
+
593
+ # Verify (auto-extracts key from KeyInfo if absent)
594
+ result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
595
+ result.valid? # => true
596
+ ----
597
+
598
+ === Supported algorithms
599
+
600
+ Digests:: SHA-1, SHA-224, SHA-256, SHA-384, SHA-512
601
+ Signature methods:: RSA-PKCS1v1.5, HMAC, ECDSA (P-256/P-384/P-521), DSA
602
+ Canonicalization:: Exclusive C14N 1.0, Inclusive C14N 1.0, Inclusive C14N 1.1
603
+ Transforms:: base64, Enveloped Signature
604
+
605
+ Adding a custom algorithm means declaring `identifier "http://..."` on a
606
+ subclass — no edits to existing code. See
607
+ link:docs/signature/algorithms.md[the algorithms doc].
608
+
609
+ === Canonicalization (Moxml::C14n)
610
+
611
+ C14N is a top-level moxml feature (sibling to XPath, Builder, SAX).
612
+ Inclusive C14N is ported from the sibling `canon` gem; Exclusive C14N
613
+ is moxml-native. See link:docs/signature/c14n.md[the C14N doc].
614
+
615
+ === Documentation
616
+
617
+ * link:docs/signature/architecture.md[Architecture]
618
+ * link:docs/signature/algorithms.md[Algorithm registry]
619
+ * link:docs/signature/c14n.md[Canonicalization]
620
+ * link:docs/signature/flows.md[Signer and Verifier flows]
621
+ * link:docs/signature/key-extraction.md[Key extraction] (TODO)
622
+ * link:docs/signature/security.md[Security considerations]
623
+ * link:docs/signature/quick-reference.md[Quick reference]
624
+ * link:examples/signature/[Runnable examples]
625
+
562
626
  == Working with documents
563
627
 
564
628
  === Using the builder pattern
@@ -0,0 +1,108 @@
1
+ # Algorithm registry
2
+
3
+ The `Moxml::Signature::Algorithms` module is the open-closed hub for
4
+ all W3C XML Signature algorithms. Algorithms are identified by URI;
5
+ the registry maps URI → class for each of four categories:
6
+
7
+ | Category | W3C spec section | Base class |
8
+ | -------------------- | ---------------- | --------------------------------------------- |
9
+ | `:digest` | §6.2 | `Algorithms::DigestBase` |
10
+ | `:signature_method` | §6.3, §6.4 | `Algorithms::SignatureMethodBase` |
11
+ | `:canonicalization` | §6.5 | `Algorithms::CanonicalizationBase` |
12
+ | `:transform` | §6.6 | `Algorithms::TransformBase` |
13
+
14
+ ## Built-in algorithms
15
+
16
+ ### Digests (§6.2)
17
+
18
+ | URI | Class |
19
+ | ---------------------------------------------------------- | ---------------- |
20
+ | `http://www.w3.org/2000/09/xmldsig#sha1` | `SHA1` |
21
+ | `http://www.w3.org/2001/04/xmldsig-more#sha224` | `SHA224` |
22
+ | `http://www.w3.org/2001/04/xmlenc#sha256` (REQUIRED) | `SHA256` |
23
+ | `http://www.w3.org/2001/04/xmldsig-more#sha384` | `SHA384` |
24
+ | `http://www.w3.org/2001/04/xmlenc#sha512` | `SHA512` |
25
+
26
+ ### Signature methods (§6.3, §6.4)
27
+
28
+ | URI | Class | Notes |
29
+ | ---------------------------------------------------------- | ---------------- | ----- |
30
+ | `…xmldsig#rsa-sha1` | `RsaPkcs1Sha` | Verification only (BP: SHA-1 discouraged) |
31
+ | `…xmldsig-more#rsa-sha224` | `RsaPkcs1Sha` | |
32
+ | `…xmldsig-more#rsa-sha256` (REQUIRED) | `RsaPkcs1Sha` | |
33
+ | `…xmldsig-more#rsa-sha384` | `RsaPkcs1Sha` | |
34
+ | `…xmldsig-more#rsa-sha512` | `RsaPkcs1Sha` | |
35
+ | `…xmldsig#hmac-sha1` | `HmacSha` | Truncation enforced per §4.4.2 |
36
+ | `…xmldsig-more#hmac-sha224` | `HmacSha` | |
37
+ | `…xmldsig-more#hmac-sha256` (REQUIRED) | `HmacSha` | |
38
+ | `…xmldsig-more#hmac-sha384` | `HmacSha` | |
39
+ | `…xmldsig-more#hmac-sha512` | `HmacSha` | |
40
+ | `…xmldsig-more#ecdsa-sha1` | `EcdsaSha` | |
41
+ | `…xmldsig-more#ecdsa-sha224` | `EcdsaSha` | |
42
+ | `…xmldsig-more#ecdsa-sha256` (REQUIRED) | `EcdsaSha` | P-256/P-384/P-521 |
43
+ | `…xmldsig-more#ecdsa-sha384` | `EcdsaSha` | |
44
+ | `…xmldsig-more#ecdsa-sha512` | `EcdsaSha` | |
45
+ | `…xmldsig#dsa-sha1` | `DsaSha` | |
46
+ | `…xmldsig11#dsa-sha256` | `DsaSha` | |
47
+
48
+ ### Canonicalization (§6.5)
49
+
50
+ | URI | Engine |
51
+ | ---------------------------------------------------------- | ---------------------------- |
52
+ | `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` | `Moxml::C14n::Inclusive10` (canon-ported) |
53
+ | `…REC-xml-c14n-20010315#WithComments` | same, `with_comments: true` |
54
+ | `http://www.w3.org/2006/12/xml-c14n11` | `Moxml::C14n::Inclusive11` |
55
+ | `…xml-c14n11#WithComments` | same, `with_comments: true` |
56
+ | `http://www.w3.org/2001/10/xml-exc-c14n#` | `Moxml::C14n::Exclusive` (moxml-native) |
57
+ | `…xml-exc-c14n#WithComments` | same, `with_comments: true` |
58
+
59
+ ### Transforms (§6.6)
60
+
61
+ | URI | Class |
62
+ | ---------------------------------------------------------- | ------------------------------ |
63
+ | `http://www.w3.org/2000/09/xmldsig#base64` | `Base64Transform` |
64
+ | `http://www.w3.org/2000/09/xmldsig#enveloped-signature` | `EnvelopedSignatureTransform` |
65
+
66
+ Canonicalization algorithms can also be used as transforms per §6.6.1.
67
+ The `TransformPipeline` looks them up in the canonicalization registry
68
+ as a fallback.
69
+
70
+ ## Adding a custom algorithm
71
+
72
+ ```ruby
73
+ require "moxml/signature"
74
+
75
+ module MyAlgo
76
+ class SHA3_256 < Moxml::Signature::Algorithms::DigestBase
77
+ identifier "http://www.w3.org/2007/xmldsig-more#sha3-256"
78
+
79
+ def compute_digest(data)
80
+ OpenSSL::Digest.digest("SHA3-256", data)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Now the URI resolves:
86
+ Moxml::Signature::Algorithms.lookup(
87
+ :digest,
88
+ "http://www.w3.org/2007/xmldsig-more#sha3-256",
89
+ )
90
+ # => MyAlgo::SHA3_256
91
+ ```
92
+
93
+ The `identifier` declaration registers the class on load. No edits to
94
+ existing code are required — pure OCP.
95
+
96
+ ## API
97
+
98
+ ```ruby
99
+ Algorithms.lookup(:digest, uri) # → class, raises UnknownAlgorithm
100
+ Algorithms.registered?(:digest, uri) # → bool
101
+ Algorithms[:digest] # → { uri => class, ... }
102
+ ```
103
+
104
+ ## Lazy loading
105
+
106
+ The registry autoloads built-in algorithm classes on first lookup
107
+ (`load_builtins!` references each constant, triggering autoload).
108
+ Custom algorithms register themselves on `require` of their file.
@@ -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
+ ```