moxml 0.1.24 → 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/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +188 -0
- data/Rakefile +116 -4
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter.rb +5 -3
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/config.rb +1 -1
- 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/moxml.gemspec +1 -1
- 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/adapter/platform_spec.rb +64 -46
- 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/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- metadata +115 -2
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Security considerations
|
|
2
|
+
|
|
3
|
+
XML signature is a complex spec with a long history of attacks. This
|
|
4
|
+
document explains what `Moxml::Signature` does to mitigate them and
|
|
5
|
+
what the application is responsible for.
|
|
6
|
+
|
|
7
|
+
## Built-in mitigations
|
|
8
|
+
|
|
9
|
+
### Best Practice 1 — authenticate before transforms (DONE)
|
|
10
|
+
|
|
11
|
+
The Verifier validates `SignatureValue` before running any reference
|
|
12
|
+
transforms. A hostile signature with expensive XSLT or XPath transforms
|
|
13
|
+
cannot consume server resources unless its `SignatureValue` verifies
|
|
14
|
+
against a key the application trusts.
|
|
15
|
+
|
|
16
|
+
### Best Practice 26 — HMAC truncation floor (DONE)
|
|
17
|
+
|
|
18
|
+
`HMACOutputLength` values below `max(hash_bits/2, 80)` are rejected
|
|
19
|
+
during algorithm instantiation (spec §4.4.2). Signatures with
|
|
20
|
+
sub-minimum truncation are deemed invalid.
|
|
21
|
+
|
|
22
|
+
### Best Practice 11 — opaque certificate handling (DONE)
|
|
23
|
+
|
|
24
|
+
`X509Certificate` payloads are decoded as raw DER and passed to
|
|
25
|
+
`OpenSSL::X509::Certificate.new`. We never re-encode certificates, so
|
|
26
|
+
the signature on the certificate itself is preserved.
|
|
27
|
+
|
|
28
|
+
## What the application must do
|
|
29
|
+
|
|
30
|
+
The library cannot make trust decisions; the application must.
|
|
31
|
+
|
|
32
|
+
### Best Practice 2 — establish trust in the key
|
|
33
|
+
|
|
34
|
+
Just because `SignatureValue` verifies against a public key in
|
|
35
|
+
`KeyInfo` does NOT mean the signature should be trusted. The key must
|
|
36
|
+
come from a trusted source:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Application-side: pin the expected key, ignore KeyInfo entirely
|
|
40
|
+
result = Moxml::Signature.verify(
|
|
41
|
+
context: ctx,
|
|
42
|
+
document: doc,
|
|
43
|
+
key: trusted_public_key, # do NOT rely on KeyInfo extraction
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Or, with certificate validation:
|
|
47
|
+
cert = OpenSSL::X509::Certificate.new(cert_der)
|
|
48
|
+
store = OpenSSL::X509::Store.new
|
|
49
|
+
store.add_trust_file("cacert.pem")
|
|
50
|
+
unless store.verify(cert)
|
|
51
|
+
raise "certificate chain invalid"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = Moxml::Signature.verify(
|
|
55
|
+
context: ctx,
|
|
56
|
+
document: doc,
|
|
57
|
+
key: cert.public_key,
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Best Practice 12 — see what was signed
|
|
62
|
+
|
|
63
|
+
Use `SingleVerificationResult#references` to inspect what the signature
|
|
64
|
+
actually covers:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
result.results.first.references.each do |ref|
|
|
68
|
+
puts "#{ref.uri}: #{ref.valid?}"
|
|
69
|
+
# Confirm ref.uri matches what the application expects to be signed.
|
|
70
|
+
# Wrapping attacks work by getting the verifier to confirm a different
|
|
71
|
+
# node than the application acts on.
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Best Practice 14 — check name AND position
|
|
76
|
+
|
|
77
|
+
When checking a reference URI, don't just verify the element name. A
|
|
78
|
+
wrapping attack moves the signed element into an `<Object>` and points
|
|
79
|
+
the reference at it; the application then acts on a different (unsigned)
|
|
80
|
+
element with the same name.
|
|
81
|
+
|
|
82
|
+
### Best Practice 8 — control external references
|
|
83
|
+
|
|
84
|
+
External URI dereferencing is **not enabled by default**. If you need
|
|
85
|
+
it, wrap the resolver in a policy that:
|
|
86
|
+
|
|
87
|
+
- Allows only same-document URIs (`#id`, `""`)
|
|
88
|
+
- Allows only specific schemes (`https://`, never `file://`)
|
|
89
|
+
- Caps size and timeout
|
|
90
|
+
- Disallows query parameters that mutate server state
|
|
91
|
+
|
|
92
|
+
## What this library does NOT do
|
|
93
|
+
|
|
94
|
+
### Not a chain validator
|
|
95
|
+
|
|
96
|
+
`Moxml::Signature` does not validate X.509 certificate chains, check
|
|
97
|
+
revocation, or evaluate certificate policies. The application is
|
|
98
|
+
responsible for these (see Best Practice 2 above).
|
|
99
|
+
|
|
100
|
+
### Not a timestamp authority
|
|
101
|
+
|
|
102
|
+
Long-lived signatures need RFC 3161 timestamps from a TSA. This library
|
|
103
|
+
does not implement timestamp verification.
|
|
104
|
+
|
|
105
|
+
### Not a wrapping-attack detector
|
|
106
|
+
|
|
107
|
+
Wrapping attacks succeed when the application acts on a different node
|
|
108
|
+
than the one the signature actually covers. The library returns the
|
|
109
|
+
list of references; the application must verify they cover the right
|
|
110
|
+
nodes.
|
|
111
|
+
|
|
112
|
+
## SHA-1 warning
|
|
113
|
+
|
|
114
|
+
The W3C spec marks SHA-1 as REQUIRED for backwards compatibility but
|
|
115
|
+
DISCOURAGED for new signatures. Cryptanalytic advances (SHAttered,
|
|
116
|
+
2017) demonstrated practical collisions. Use SHA-256 or stronger for
|
|
117
|
+
new signatures.
|
|
118
|
+
|
|
119
|
+
## HMAC security
|
|
120
|
+
|
|
121
|
+
HMAC signatures require a shared secret. Any verifier with the secret
|
|
122
|
+
can forge signatures. Use distinct keys for signing vs. encryption
|
|
123
|
+
(Best Practice 27).
|
|
124
|
+
|
|
125
|
+
## Limited XPath support
|
|
126
|
+
|
|
127
|
+
The library implements the Enveloped Signature transform directly
|
|
128
|
+
(no XPath). XPath Filter and XSLT transforms are not implemented;
|
|
129
|
+
Best Practices 3, 5, 6, 22 say avoid them.
|
|
130
|
+
|
|
131
|
+
## Performance and DoS
|
|
132
|
+
|
|
133
|
+
Canonicalization is O(N) for tree size. XSLT and complex XPath are
|
|
134
|
+
not supported, eliminating the most common DoS vectors (Best Practices
|
|
135
|
+
3, 5, 6). Wrap calls in a timeout if processing untrusted input:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
require "timeout"
|
|
139
|
+
|
|
140
|
+
Timeout.timeout(5) do
|
|
141
|
+
Moxml::Signature.verify(context: ctx, document: untrusted_doc, key:)
|
|
142
|
+
end
|
|
143
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: auto-extract the verification key from a KeyInfo that
|
|
4
|
+
# contains an embedded X509Certificate. The Verifier derives the
|
|
5
|
+
# OpenSSL key without application help.
|
|
6
|
+
#
|
|
7
|
+
# Run with: bundle exec ruby examples/signature/auto_key_extraction.rb
|
|
8
|
+
|
|
9
|
+
require "moxml"
|
|
10
|
+
require "moxml/signature"
|
|
11
|
+
|
|
12
|
+
ctx = Moxml.new(:nokogiri)
|
|
13
|
+
|
|
14
|
+
fixture = File.expand_path(
|
|
15
|
+
"../../spec/fixtures/xmldsig/sign3-result.xml",
|
|
16
|
+
__dir__,
|
|
17
|
+
)
|
|
18
|
+
xml = File.read(fixture)
|
|
19
|
+
doc = ctx.parse(xml)
|
|
20
|
+
|
|
21
|
+
# No explicit key — Verifier auto-extracts from X509Certificate.
|
|
22
|
+
result = Moxml::Signature.verify(context: ctx, document: doc)
|
|
23
|
+
puts "Auto-extracted from X509Certificate: #{result.valid?}"
|
|
24
|
+
puts "Signature count: #{result.signature_count}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: enveloped RSA-SHA256 signature with verification.
|
|
4
|
+
#
|
|
5
|
+
# Run with: bundle exec ruby examples/signature/enveloped_rsa.rb
|
|
6
|
+
|
|
7
|
+
require "moxml"
|
|
8
|
+
require "moxml/signature"
|
|
9
|
+
require "openssl"
|
|
10
|
+
|
|
11
|
+
ctx = Moxml.new(:nokogiri)
|
|
12
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
13
|
+
|
|
14
|
+
doc = ctx.parse("<doc><greeting>Hello, World!</greeting></doc>")
|
|
15
|
+
puts "=== Before signing ==="
|
|
16
|
+
puts doc.to_xml
|
|
17
|
+
|
|
18
|
+
signature = Moxml::Signature.sign(
|
|
19
|
+
context: ctx,
|
|
20
|
+
document: doc,
|
|
21
|
+
key: key,
|
|
22
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
23
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
24
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
25
|
+
reference_uri: "",
|
|
26
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
|
|
30
|
+
doc.root.add_child(serialized.root)
|
|
31
|
+
|
|
32
|
+
puts ""
|
|
33
|
+
puts "=== After signing ==="
|
|
34
|
+
puts doc.to_xml
|
|
35
|
+
|
|
36
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
37
|
+
puts ""
|
|
38
|
+
puts "=== Verification ==="
|
|
39
|
+
puts "Valid: #{result.valid?}"
|
|
40
|
+
|
|
41
|
+
# Tamper test
|
|
42
|
+
doc2 = ctx.parse(doc.to_xml(indent: 0))
|
|
43
|
+
doc2.at_xpath("//greeting").text = "Goodbye!"
|
|
44
|
+
tampered = Moxml::Signature.verify(context: ctx, document: doc2, key: key)
|
|
45
|
+
puts "Tampered: #{tampered.valid?} (expected false)"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: HMAC-SHA256 signature with a shared secret.
|
|
4
|
+
#
|
|
5
|
+
# Run with: bundle exec ruby examples/signature/hmac.rb
|
|
6
|
+
|
|
7
|
+
require "moxml"
|
|
8
|
+
require "moxml/signature"
|
|
9
|
+
|
|
10
|
+
ctx = Moxml.new(:nokogiri)
|
|
11
|
+
secret = "super-secret-shared-key"
|
|
12
|
+
|
|
13
|
+
doc = ctx.parse("<message><body>hello</body></message>")
|
|
14
|
+
|
|
15
|
+
signature = Moxml::Signature.sign(
|
|
16
|
+
context: ctx,
|
|
17
|
+
document: doc,
|
|
18
|
+
key: secret,
|
|
19
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
|
20
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
21
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
22
|
+
reference_uri: "",
|
|
23
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
|
|
27
|
+
doc.root.add_child(serialized.root)
|
|
28
|
+
|
|
29
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: secret)
|
|
30
|
+
puts "Valid: #{result.valid?}"
|
|
31
|
+
|
|
32
|
+
wrong = Moxml::Signature.verify(context: ctx, document: doc, key: "wrong")
|
|
33
|
+
puts "Wrong secret: #{wrong.valid?} (expected false)"
|
|
@@ -35,6 +35,10 @@ require "moxml/sax/element_handler"
|
|
|
35
35
|
require "moxml/sax/block_handler"
|
|
36
36
|
require "moxml/sax/namespace_splitter"
|
|
37
37
|
require "moxml/adapter/rexml"
|
|
38
|
+
require "moxml/adapter/customized_oga"
|
|
39
|
+
require "moxml/adapter/customized_oga/xml_declaration"
|
|
40
|
+
require "moxml/adapter/customized_oga/xml_generator"
|
|
41
|
+
require "moxml/adapter/oga"
|
|
38
42
|
require "moxml/document_builder"
|
|
39
43
|
require "moxml/builder"
|
|
40
44
|
require "moxml/context"
|
data/lib/moxml/adapter.rb
CHANGED
|
@@ -11,9 +11,11 @@ module Moxml
|
|
|
11
11
|
AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
12
12
|
|
|
13
13
|
# Adapters that work under the Opal (JavaScript) runtime.
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
14
|
+
# Oga is pure Ruby and designed with Opal compatibility in mind — it is the
|
|
15
|
+
# canonical XML parser for the JavaScript runtime. REXML is also pure Ruby
|
|
16
|
+
# but requires extensive runtime compat shims (regex features like /n,
|
|
17
|
+
# \u{...}, (?-mix:...) don't transpile cleanly), so it is opt-in only.
|
|
18
|
+
OPAL_AVAILABLE_ADAPTERS = %i[oga rexml].freeze
|
|
17
19
|
|
|
18
20
|
# Registry mapping adapter names to their class name suffixes.
|
|
19
21
|
# Special cases (like :headed_ox → "HeadedOx") live here instead of
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Attribute axis handler for inclusive C14N.
|
|
6
|
+
# Implements W3C C14N 1.0 §2.4 / 1.1 §2.4 attribute rendering,
|
|
7
|
+
# including resolution of simple inheritable attributes (xml:lang,
|
|
8
|
+
# xml:space) for document subsets.
|
|
9
|
+
class AttributeHandler
|
|
10
|
+
attr_reader :encoder
|
|
11
|
+
|
|
12
|
+
def initialize(encoder)
|
|
13
|
+
@encoder = encoder
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_attributes(element, output, omitted_ancestors = [])
|
|
17
|
+
return unless element.in_node_set?
|
|
18
|
+
|
|
19
|
+
attributes = collect_attributes(element, omitted_ancestors)
|
|
20
|
+
attributes.each do |attr|
|
|
21
|
+
output << " "
|
|
22
|
+
output << attr.qname
|
|
23
|
+
output << '="'
|
|
24
|
+
output << encoder.encode_attribute(attr.value)
|
|
25
|
+
output << '"'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def collect_attributes(element, omitted_ancestors)
|
|
32
|
+
attributes = element.sorted_attribute_nodes.select(&:in_node_set?)
|
|
33
|
+
|
|
34
|
+
return attributes if omitted_ancestors.empty?
|
|
35
|
+
|
|
36
|
+
inherited = collect_inherited_attributes(element, omitted_ancestors)
|
|
37
|
+
merge_attributes(attributes, inherited)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Walk omitted ancestors to collect simple inheritable attributes
|
|
41
|
+
# not already declared on the element. Per C14N 1.1 §2.4, these
|
|
42
|
+
# are inherited from the nearest ancestor in which they are declared.
|
|
43
|
+
def collect_inherited_attributes(element, omitted_ancestors)
|
|
44
|
+
inherited = []
|
|
45
|
+
seen = Set.new
|
|
46
|
+
|
|
47
|
+
element.attribute_nodes.each do |attr|
|
|
48
|
+
seen.add(attr.name) if attr.simple_inheritable?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
omitted_ancestors.reverse_each do |ancestor|
|
|
52
|
+
ancestor.attribute_nodes.each do |attr|
|
|
53
|
+
next unless attr.simple_inheritable?
|
|
54
|
+
next if seen.include?(attr.name)
|
|
55
|
+
|
|
56
|
+
inherited << attr
|
|
57
|
+
seen.add(attr.name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
inherited
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def merge_attributes(element_attrs, inherited_attrs)
|
|
65
|
+
(element_attrs + inherited_attrs).sort_by do |attr|
|
|
66
|
+
[attr.namespace_uri.to_s, attr.local_name]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Character encoder for C14N output. Handles the entity escaping
|
|
6
|
+
# mandated by W3C C14N 1.0 §2.4 / 1.1 §2.4 for text and attribute values.
|
|
7
|
+
class CharacterEncoder
|
|
8
|
+
TEXT_ESCAPES = {
|
|
9
|
+
"&" => "&",
|
|
10
|
+
"<" => "<",
|
|
11
|
+
">" => ">",
|
|
12
|
+
"\r" => "
",
|
|
13
|
+
}.freeze
|
|
14
|
+
ATTRIBUTE_ESCAPES = TEXT_ESCAPES.merge(
|
|
15
|
+
'"' => """,
|
|
16
|
+
"\t" => "	",
|
|
17
|
+
"\n" => "
",
|
|
18
|
+
).freeze
|
|
19
|
+
|
|
20
|
+
def encode_text(text)
|
|
21
|
+
text.to_s.gsub(/[&<>\r]/, TEXT_ESCAPES)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def encode_attribute(value)
|
|
25
|
+
value.to_s.gsub(/[&<"\t\n\r]/, ATTRIBUTE_ESCAPES)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Builds a C14N data model from a Moxml::Node tree (or XML string).
|
|
6
|
+
#
|
|
7
|
+
# The data model exists because canonicalization needs:
|
|
8
|
+
# - sorted namespace and attribute axes (spec §2.3, §2.4)
|
|
9
|
+
# - in_node_set flags for subset canonicalization (spec §3)
|
|
10
|
+
# - xml:* inheritable attribute resolution
|
|
11
|
+
#
|
|
12
|
+
# Ported from canon (lutaml/canon) — adapted to build directly from
|
|
13
|
+
# Moxml::Node rather than via a separate Nokogiri pass. Matches
|
|
14
|
+
# canon's document-level node iteration (PIs and comments outside
|
|
15
|
+
# the document root element).
|
|
16
|
+
class DataModel
|
|
17
|
+
def self.from_xml(xml_string)
|
|
18
|
+
from_node(::Moxml.parse(xml_string))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_node(moxml_node)
|
|
22
|
+
return build_from_document(moxml_node) if moxml_node.is_a?(::Moxml::Document)
|
|
23
|
+
|
|
24
|
+
root = Nodes::RootNode.new
|
|
25
|
+
root.add_child(build_node(moxml_node))
|
|
26
|
+
root
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build from a Moxml::Document. Matches canon's Nokogiri path:
|
|
30
|
+
# the root element is added first, then all other document-level
|
|
31
|
+
# children (PIs, comments) in document order.
|
|
32
|
+
def self.build_from_document(document)
|
|
33
|
+
root = Nodes::RootNode.new
|
|
34
|
+
|
|
35
|
+
if document.root
|
|
36
|
+
root.add_child(build_element_node(document.root))
|
|
37
|
+
# Iterate ALL document children — not just the root element.
|
|
38
|
+
# This captures PIs and comments that appear outside the
|
|
39
|
+
# document element, which are part of the canonical form.
|
|
40
|
+
document.children.each do |child|
|
|
41
|
+
next if child.equal?(document.root)
|
|
42
|
+
next if child.is_a?(::Moxml::Element)
|
|
43
|
+
|
|
44
|
+
built = build_node(child)
|
|
45
|
+
root.add_child(built) if built
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
root
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.build_node(moxml_node)
|
|
53
|
+
case moxml_node
|
|
54
|
+
when ::Moxml::Element then build_element_node(moxml_node)
|
|
55
|
+
when ::Moxml::Text then build_text_node(moxml_node)
|
|
56
|
+
when ::Moxml::Comment then build_comment_node(moxml_node)
|
|
57
|
+
when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.build_element_node(moxml_element)
|
|
62
|
+
ns = moxml_element.namespace
|
|
63
|
+
element = Nodes::ElementNode.new(
|
|
64
|
+
name: moxml_element.name,
|
|
65
|
+
namespace_uri: ns&.uri,
|
|
66
|
+
prefix: ns&.prefix,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
build_namespace_nodes(moxml_element, element)
|
|
70
|
+
build_attribute_nodes(moxml_element, element)
|
|
71
|
+
|
|
72
|
+
moxml_element.children.each do |child|
|
|
73
|
+
built = build_node(child)
|
|
74
|
+
element.add_child(built) if built
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
element
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.build_namespace_nodes(moxml_element, element)
|
|
81
|
+
moxml_element.in_scope_namespaces.each do |ns|
|
|
82
|
+
element.add_namespace(
|
|
83
|
+
Nodes::NamespaceNode.new(prefix: ns.prefix || "", uri: ns.uri),
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return if element.namespace_nodes.any? { |n| n.prefix == "xml" }
|
|
88
|
+
|
|
89
|
+
element.add_namespace(
|
|
90
|
+
Nodes::NamespaceNode.new(prefix: "xml", uri: XML_URI),
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.build_attribute_nodes(moxml_element, element)
|
|
95
|
+
moxml_element.attributes.each do |attr|
|
|
96
|
+
ns = attr.namespace
|
|
97
|
+
element.add_attribute(
|
|
98
|
+
Nodes::AttributeNode.new(
|
|
99
|
+
name: attr.name,
|
|
100
|
+
value: attr.value,
|
|
101
|
+
namespace_uri: ns&.uri,
|
|
102
|
+
prefix: ns&.prefix,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.build_text_node(moxml_text)
|
|
109
|
+
Nodes::TextNode.new(value: moxml_text.content)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.build_comment_node(moxml_comment)
|
|
113
|
+
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.build_pi_node(moxml_pi)
|
|
117
|
+
Nodes::ProcessingInstructionNode.new(
|
|
118
|
+
target: moxml_pi.target || moxml_pi.name,
|
|
119
|
+
data: moxml_pi.content || "",
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.mark_all(node, value)
|
|
124
|
+
node.in_node_set = value
|
|
125
|
+
node.children.each { |child| mark_all(child, value) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.mark_subset(root_node, matched)
|
|
129
|
+
matched.each { |node| mark_node_and_descendants(node) }
|
|
130
|
+
root_node.in_node_set = true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.mark_node_and_descendants(node)
|
|
134
|
+
node.in_node_set = true
|
|
135
|
+
node.children.each { |child| mark_node_and_descendants(child) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private_class_method :build_from_document, :build_node,
|
|
139
|
+
:build_element_node, :build_namespace_nodes,
|
|
140
|
+
:build_attribute_nodes, :build_text_node,
|
|
141
|
+
:build_comment_node, :build_pi_node,
|
|
142
|
+
:mark_node_and_descendants
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|