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
@@ -0,0 +1,132 @@
1
+ # Signing and verification flows
2
+
3
+ ## Signer (spec §3.1)
4
+
5
+ ```
6
+ Moxml::Signature.sign(context:, document:, key:, **options)
7
+
8
+
9
+ build a Model::Signature with the requested algorithms
10
+
11
+
12
+ Signer#sign
13
+
14
+ ├── for each Reference in SignedInfo:
15
+ │ ├── ReferenceResolver.resolve(uri) → node or octets
16
+ │ ├── TransformPipeline.apply(input, transforms)
17
+ │ ├── TransformPipeline.to_octets(transformed, reference)
18
+ │ └── DigestMethod#digest_base64(canonical) → Reference#digest_value
19
+
20
+ ├── Serializer.serialize_signed_info(signed_info) → Moxml::Document
21
+ ├── CanonicalizationMethod#canonicalize(signed_info_root) → octets
22
+ └── SignatureMethod#sign(canonical_octets, key) → SignatureValue
23
+ ```
24
+
25
+ The Signer never touches algorithms directly — every step delegates to
26
+ the algorithm registry. This is OCP: new algorithms are added without
27
+ touching the Signer.
28
+
29
+ ## Verifier (spec §3.2 + Best Practice 1)
30
+
31
+ ```
32
+ Moxml::Signature.verify(context:, document:, key: nil, key_map: {})
33
+
34
+
35
+ find all ds:Signature elements in the document
36
+
37
+ ▼ for each Signature
38
+ verify_one(signature_element)
39
+
40
+ ├── Parser.parse(signature_element) → Model::Signature
41
+
42
+ ├── verify_signature_value FIRST (Best Practice 1)
43
+ │ ├── find original SignedInfo element (do NOT re-serialize)
44
+ │ ├── CanonicalizationMethod#canonicalize(signed_info_elem)
45
+ │ ├── resolve_key(signature) (KeyExtractor if no key:)
46
+ │ └── SignatureMethod#verify(canonical, key, sig_value)
47
+
48
+ └── if signature value verifies:
49
+ for each Reference:
50
+ ├── ReferenceResolver.resolve(uri)
51
+ ├── TransformPipeline.apply(...)
52
+ ├── TransformPipeline.to_octets(...)
53
+ └── DigestMethod#digest_base64 vs Reference#digest_value
54
+ ```
55
+
56
+ ### Best Practice 1: authenticate before transforms
57
+
58
+ The verifier deliberately runs `SignatureValue` validation BEFORE
59
+ applying any transforms. Reason: a malicious signature could include
60
+ XSLT or expensive XPath transforms. Best Practice 1 says only run
61
+ those after authenticating the signer.
62
+
63
+ If `SignatureValue` doesn't verify, we skip reference transforms
64
+ entirely. The result object reports `signature_valid?: false` and
65
+ `references: []`.
66
+
67
+ ### Why we canonicalize the original element
68
+
69
+ The verifier canonicalizes the SignedInfo **as it appears in the
70
+ document**, not a re-serialized model. Reason: re-serialization could
71
+ change namespace prefixes (e.g., default-namespace SignedInfo might
72
+ become `ds:SignedInfo`), breaking the byte-exact match the signature
73
+ was computed against.
74
+
75
+ The Signer serializes (it has to — it's building new XML). The Verifier
76
+ has the original element from the document and uses it directly. This
77
+ asymmetry is intentional and necessary for cross-verification with
78
+ libxmlsec1-produced signatures.
79
+
80
+ ### Errors are captured, not raised
81
+
82
+ `SingleVerificationResult#error` carries any `VerificationError` or
83
+ `UnknownAlgorithm` that caused failure. The verifier returns a result
84
+ object rather than raising, so a hostile signature cannot panic the
85
+ application. Use `result.results.first.error` for debugging.
86
+
87
+ ## Key resolution
88
+
89
+ ```ruby
90
+ Moxml::Signature.verify(context:, document:) # no key:
91
+ ```
92
+
93
+ When no `key:` is passed, the Verifier uses `KeyExtractor` to derive
94
+ one from the signature's `KeyInfo`:
95
+
96
+ 1. **X509Certificate** (preferred) — decode base64 DER →
97
+ `OpenSSL::X509::Certificate` → `.public_key`
98
+ 2. **RSAKeyValue** — reconstruct via ASN.1 (OpenSSL 3.x dropped
99
+ `RSA.new(n, e)`)
100
+ 3. **DSAKeyValue** — reconstruct via ASN.1
101
+ 4. **ECKeyValue** — build SubjectPublicKeyInfo DER for the named curve
102
+ (OpenSSL 3.x made PKey immutable)
103
+ 6. **KeyName** — look up in the application-supplied `key_map:`
104
+
105
+ OpenSSL 3.x compatibility notes are inline in `key_extractor.rb`.
106
+
107
+ ## Transform pipeline
108
+
109
+ `TransformPipeline` is shared by Signer and Verifier. Each Transform
110
+ in the chain declares `input_type` (`:octets` or `:nodeset`) and
111
+ `output_type`; the pipeline coerces types between transforms per
112
+ spec §4.4.3.2:
113
+
114
+ - `octets → nodeset`: parse as XML via the caller's `context:`
115
+ - `nodeset → octets`: apply inclusive C14N 1.0
116
+
117
+ Final conversion to octets (for digesting) uses the last canonicalization
118
+ transform in the chain if any, else inclusive C14N 1.0 as the spec default.
119
+
120
+ ## Result objects
121
+
122
+ ```ruby
123
+ result = Moxml::Signature.verify(context:, document:, key:)
124
+ result.valid? # → bool (all signatures valid)
125
+ result.signature_count # → Integer
126
+ result.results # → [SingleVerificationResult, ...]
127
+ result.results.first.signature_valid?
128
+ result.results.first.references # → [ReferenceResult, ...]
129
+ result.results.first.references.first.valid?
130
+ result.results.first.error # → Exception or nil
131
+ result.failing # → [SingleVerificationResult, ...]
132
+ ```
@@ -0,0 +1,115 @@
1
+ # Quick reference
2
+
3
+ ## Public API
4
+
5
+ ```ruby
6
+ # Sign a document
7
+ signature = Moxml::Signature.sign(
8
+ context:, # Moxml::Context (from Moxml.new(:nokogiri))
9
+ document:, # Moxml::Document to sign
10
+ key:, # OpenSSL::PKey::* or HMAC secret String
11
+ signature_method:, # algorithm URI
12
+ canonicalization_method:, # algorithm URI
13
+ digest_method:, # algorithm URI
14
+ reference_uri:, # "" for whole document
15
+ transforms:, # array of algorithm URIs
16
+ key_info: nil, # optional Model::KeyInfo
17
+ signature_id: nil, # optional Id attribute
18
+ )
19
+
20
+ # Verify a document
21
+ result = Moxml::Signature.verify(
22
+ context:,
23
+ document:,
24
+ key: nil, # optional; auto-extracted from KeyInfo if absent
25
+ key_map: {}, # for KeyName-based resolution
26
+ )
27
+ ```
28
+
29
+ ## Algorithm URIs
30
+
31
+ ### Digests
32
+ - `http://www.w3.org/2000/09/xmldsig#sha1` (verification only)
33
+ - `http://www.w3.org/2001/04/xmlenc#sha256` (REQUIRED)
34
+ - `http://www.w3.org/2001/04/xmlenc#sha512`
35
+ - `http://www.w3.org/2001/04/xmldsig-more#sha224`
36
+ - `http://www.w3.org/2001/04/xmldsig-more#sha384`
37
+
38
+ ### Signature methods
39
+ - RSA-PKCS1v1.5: `http://www.w3.org/2001/04/xmldsig-more#rsa-sha{1,224,256,384,512}`
40
+ - HMAC: `http://www.w3.org/2001/04/xmldsig-more#hmac-sha{1,224,256,384,512}`
41
+ - ECDSA: `http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha{1,224,256,384,512}`
42
+ - DSA: `http://www.w3.org/2000/09/xmldsig#dsa-sha1`, `http://www.w3.org/2009/xmldsig11#dsa-sha256`
43
+
44
+ ### Canonicalization
45
+ - Exclusive: `http://www.w3.org/2001/10/xml-exc-c14n#` (add `#WithComments`)
46
+ - Inclusive 1.0: `http://www.w3.org/TR/2001/REC-xml-c14n-20010315`
47
+ - Inclusive 1.1: `http://www.w3.org/2006/12/xml-c14n11`
48
+
49
+ ### Transforms
50
+ - `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
51
+ - `http://www.w3.org/2000/09/xmldsig#base64`
52
+
53
+ ## Common algorithm combinations
54
+
55
+ ### Enveloped RSA-SHA256 (most common)
56
+ ```ruby
57
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
58
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
59
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
60
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
61
+ ```
62
+
63
+ ### HMAC-SHA256
64
+ ```ruby
65
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
66
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
67
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
68
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
69
+ ```
70
+
71
+ ### ECDSA-SHA256 (P-256)
72
+ ```ruby
73
+ signature_method: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
74
+ canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
75
+ digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
76
+ transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
77
+ ```
78
+
79
+ ## Result inspection
80
+
81
+ ```ruby
82
+ result.valid? # → bool (all signatures)
83
+ result.signature_count # → Integer
84
+ result.failing # → [SingleVerificationResult]
85
+
86
+ result.results.first.signature_valid? # → bool (crypto verify)
87
+ result.results.first.references # → [ReferenceResult]
88
+ result.results.first.failing_references # → [ReferenceResult]
89
+ result.results.first.error # → Exception or nil (debugging)
90
+ ```
91
+
92
+ ## C14N top-level API
93
+
94
+ ```ruby
95
+ Moxml::C14n.canonicalize(node_or_xml, with_comments: false)
96
+ Moxml::C14n.canonicalize_exclusive(node_or_xml, with_comments: false,
97
+ inclusive_namespaces: [])
98
+ ```
99
+
100
+ ## Errors
101
+
102
+ ```ruby
103
+ Moxml::Signature::Error # base
104
+ Moxml::Signature::SignatureError # generic
105
+ Moxml::Signature::UnknownAlgorithm # URI not in registry
106
+ Moxml::Signature::DuplicateAlgorithm # URI registered twice
107
+ Moxml::Signature::SigningError # signing failed
108
+ Moxml::Signature::VerificationError # verification failed
109
+ Moxml::Signature::ReferenceDigestMismatch # digest mismatch
110
+ Moxml::Signature::SignatureValueMismatch # crypto verify failed
111
+ Moxml::Signature::TransformError # transform pipeline
112
+ Moxml::Signature::CanonicalizationError # C14N failure
113
+ Moxml::Signature::MalformedSignatureError # XML schema violation
114
+ Moxml::Signature::SignatureKeyError # wrong key type
115
+ ```
@@ -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)"
@@ -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
+ "&" => "&amp;",
10
+ "<" => "&lt;",
11
+ ">" => "&gt;",
12
+ "\r" => "&#xD;",
13
+ }.freeze
14
+ ATTRIBUTE_ESCAPES = TEXT_ESCAPES.merge(
15
+ '"' => "&quot;",
16
+ "\t" => "&#x9;",
17
+ "\n" => "&#xA;",
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