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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/README.adoc +64 -0
- 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/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/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 +29 -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.rb +2 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- 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/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/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
- metadata +111 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d829252809a02c146a09b8458ec5d9a297e5a31e91e6b04b6b6eda27a31e30a9
|
|
4
|
+
data.tar.gz: '09018d566262901ac5fe3fd864cd80e52dfe05f449e5b9e03054624044cab1af'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: `&` → `&`, `<` → `<`, `>` → `>`
|
|
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
|
+
```
|