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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +1 -1
  3. data/.github/workflows/opal.yml +18 -1
  4. data/.github/workflows/rake.yml +32 -3
  5. data/.github/workflows/release.yml +9 -0
  6. data/.github/workflows/round-trip.yml +72 -9
  7. data/.gitmodules +6 -0
  8. data/.rubocop_todo.yml +2 -0
  9. data/Gemfile +9 -1
  10. data/README.adoc +188 -0
  11. data/Rakefile +116 -4
  12. data/docs/signature/algorithms.md +108 -0
  13. data/docs/signature/architecture.md +148 -0
  14. data/docs/signature/c14n.md +110 -0
  15. data/docs/signature/examples.md +109 -0
  16. data/docs/signature/flows.md +132 -0
  17. data/docs/signature/quick-reference.md +115 -0
  18. data/docs/signature/security.md +143 -0
  19. data/examples/signature/auto_key_extraction.rb +24 -0
  20. data/examples/signature/enveloped_rsa.rb +45 -0
  21. data/examples/signature/hmac.rb +33 -0
  22. data/lib/compat/opal/moxml_boot.rb +4 -0
  23. data/lib/moxml/adapter.rb +5 -3
  24. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  25. data/lib/moxml/c14n/character_encoder.rb +29 -0
  26. data/lib/moxml/c14n/data_model.rb +145 -0
  27. data/lib/moxml/c14n/exclusive.rb +188 -0
  28. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  29. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  30. data/lib/moxml/c14n/namespace_context.rb +68 -0
  31. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  32. data/lib/moxml/c14n/node.rb +42 -0
  33. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  34. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  35. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  36. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  37. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  38. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  39. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  40. data/lib/moxml/c14n/nodes.rb +17 -0
  41. data/lib/moxml/c14n/processor.rb +118 -0
  42. data/lib/moxml/c14n/writer.rb +121 -0
  43. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  44. data/lib/moxml/c14n.rb +96 -0
  45. data/lib/moxml/config.rb +1 -1
  46. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  47. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  48. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  49. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  50. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  51. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  52. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  53. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  54. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  55. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  56. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  57. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  58. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  59. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  60. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  61. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  62. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  63. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  64. data/lib/moxml/signature/algorithms.rb +113 -0
  65. data/lib/moxml/signature/errors.rb +99 -0
  66. data/lib/moxml/signature/key_extractor.rb +172 -0
  67. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  68. data/lib/moxml/signature/model/digest_method.rb +15 -0
  69. data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
  70. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  71. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  72. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  73. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  74. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  75. data/lib/moxml/signature/model/key_info.rb +21 -0
  76. data/lib/moxml/signature/model/key_value.rb +20 -0
  77. data/lib/moxml/signature/model/object_element.rb +18 -0
  78. data/lib/moxml/signature/model/reference.rb +22 -0
  79. data/lib/moxml/signature/model/signature.rb +21 -0
  80. data/lib/moxml/signature/model/signature_value.rb +16 -0
  81. data/lib/moxml/signature/model/signed_info.rb +20 -0
  82. data/lib/moxml/signature/model/transform.rb +16 -0
  83. data/lib/moxml/signature/model/transforms.rb +36 -0
  84. data/lib/moxml/signature/model.rb +35 -0
  85. data/lib/moxml/signature/parser.rb +270 -0
  86. data/lib/moxml/signature/reference_resolver.rb +79 -0
  87. data/lib/moxml/signature/reference_result.rb +22 -0
  88. data/lib/moxml/signature/serializer.rb +163 -0
  89. data/lib/moxml/signature/signer.rb +73 -0
  90. data/lib/moxml/signature/single_verification_result.rb +31 -0
  91. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  92. data/lib/moxml/signature/verification_result.rb +27 -0
  93. data/lib/moxml/signature/verifier.rb +129 -0
  94. data/lib/moxml/signature.rb +94 -0
  95. data/lib/moxml/version.rb +1 -1
  96. data/lib/moxml.rb +2 -0
  97. data/moxml.gemspec +1 -1
  98. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  99. data/reference-docs/w3c-xmldsig-core.md +400 -0
  100. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  101. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  102. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  103. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  104. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  105. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  106. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  107. data/spec/moxml/adapter/platform_spec.rb +64 -46
  108. data/spec/moxml/c14n/api_spec.rb +117 -0
  109. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  110. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  111. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  112. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  113. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  114. data/spec/moxml/opal_oga_adapter_spec.rb +14 -0
  115. data/spec/moxml/opal_oga_features_spec.rb +212 -0
  116. data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
  117. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  118. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  119. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  120. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  121. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  122. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  123. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  124. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  125. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  126. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  127. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  128. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  129. data/spec/moxml/signature/model_spec.rb +54 -0
  130. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  131. data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
  132. data/spec/spec_helper.rb +1 -1
  133. metadata +115 -2
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # DSA signature methods per FIPS 186-3.
9
+ #
10
+ # Two URIs:
11
+ # - SHA1 (1024-bit, q=160): wire format r‖s with 20-byte halves
12
+ # - SHA256 (2048-bit, q=256): wire format r‖s with N-byte halves
13
+ # where N = byte length of q (32 for SHA-256 case)
14
+ #
15
+ # OpenSSL returns DER; we convert to raw r‖s.
16
+ class DsaSha < SignatureMethodBase
17
+ PAIRINGS = {
18
+ "http://www.w3.org/2000/09/xmldsig#dsa-sha1" => "SHA1",
19
+ "http://www.w3.org/2009/xmldsig11#dsa-sha256" => "SHA256",
20
+ }.freeze
21
+
22
+ PAIRINGS.each_key { |uri| identifier uri }
23
+
24
+ def initialize(identifier_uri:, parameters: nil)
25
+ super(nil)
26
+ @identifier_uri = identifier_uri
27
+ @parameters = parameters
28
+ end
29
+
30
+ def compute_signature(data, key)
31
+ dsa = coerce_signing_key(key)
32
+ digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
33
+ der_signature = dsa.sign(digest_name, data)
34
+ der_to_raw(der_signature, coordinate_bytes_for(dsa))
35
+ end
36
+
37
+ def verify_signature(data, key, signature)
38
+ dsa = coerce_verify_key(key)
39
+ digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
40
+ n_bytes = coordinate_bytes_for(dsa)
41
+ der_signature = raw_to_der(signature, n_bytes)
42
+ dsa.verify(digest_name, der_signature, data)
43
+ rescue ArgumentError
44
+ false
45
+ end
46
+
47
+ private
48
+
49
+ def coerce_signing_key(key)
50
+ case key
51
+ when OpenSSL::PKey::DSA
52
+ key
53
+ else
54
+ raise SignatureKeyError,
55
+ "DSA signature method requires OpenSSL::PKey::DSA, " \
56
+ "got #{key.class}"
57
+ end
58
+ end
59
+
60
+ def coerce_verify_key(key)
61
+ coerce_signing_key(key)
62
+ end
63
+
64
+ def coordinate_bytes_for(dsa)
65
+ # q is the subgroup order. byte length of q.
66
+ q_bits = dsa.q.num_bits
67
+ (q_bits + 7) / 8
68
+ end
69
+
70
+ def der_to_raw(der, n_bytes)
71
+ seq = OpenSSL::ASN1.decode(der)
72
+ r = seq.value[0].value
73
+ s = seq.value[1].value
74
+ i2osp(r, n_bytes) + i2osp(s, n_bytes)
75
+ end
76
+
77
+ def raw_to_der(raw, n_bytes)
78
+ raise ArgumentError, "raw signature has wrong size" if raw.bytesize != (2 * n_bytes)
79
+
80
+ r = raw.byteslice(0, n_bytes)
81
+ s = raw.byteslice(n_bytes, n_bytes)
82
+ seq = OpenSSL::ASN1::Sequence.new([
83
+ OpenSSL::ASN1::Integer.new(asn1_integer_value(r)),
84
+ OpenSSL::ASN1::Integer.new(asn1_integer_value(s)),
85
+ ])
86
+ seq.to_der
87
+ end
88
+
89
+ def i2osp(value, length)
90
+ n = value.to_i
91
+ raise ArgumentError, "I2OSP: integer too large" if n >= (1 << (8 * length))
92
+
93
+ bytes = Array.new(length, 0)
94
+ (length - 1).downto(0) do |i|
95
+ bytes[i] = n & 0xff
96
+ n >>= 8
97
+ end
98
+ bytes.pack("C*")
99
+ end
100
+
101
+ def asn1_integer_value(octets)
102
+ octets.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # ECDSA signature methods per FIPS 186-3, bound to a digest.
9
+ #
10
+ # Spec §6.4.3 requires support for ECDSAwithSHA256 over the P-256
11
+ # curve. Recommended: P-384, P-521.
12
+ #
13
+ # The wire format is base64(I2OSP(r, n_bytes) || I2OSP(s, n_bytes))
14
+ # where n_bytes is the byte length of the base point order
15
+ # (32 for P-256, 48 for P-384, 66 for P-521).
16
+ #
17
+ # OpenSSL returns a DER-encoded ASN.1 sequence of (r, s). We convert
18
+ # to the raw r||s form on sign, and back from raw to DER on verify.
19
+ class EcdsaSha < SignatureMethodBase
20
+ PAIRINGS = {
21
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1" => "SHA1",
22
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224" => "SHA224",
23
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => "SHA256",
24
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => "SHA384",
25
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" => "SHA512",
26
+ }.freeze
27
+
28
+ # Map OpenSSL curve name → byte length of base point order.
29
+ CURVE_ORDER_BYTES = {
30
+ "prime256v1" => 32, # P-256
31
+ "secp384r1" => 48, # P-384
32
+ "secp521r1" => 66, # P-521 (ceil(521/8) = 66)
33
+ }.freeze
34
+
35
+ PAIRINGS.each_key { |uri| identifier uri }
36
+
37
+ def initialize(identifier_uri:, parameters: nil)
38
+ super(nil)
39
+ @identifier_uri = identifier_uri
40
+ @parameters = parameters
41
+ end
42
+
43
+ def compute_signature(data, key)
44
+ ec_key = coerce_key(key)
45
+ digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
46
+ der_signature = ec_key.sign(digest_name, data)
47
+ der_to_raw(der_signature, coordinate_bytes_for(ec_key))
48
+ end
49
+
50
+ def verify_signature(data, key, signature)
51
+ ec_key = coerce_key(key)
52
+ digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
53
+ n_bytes = coordinate_bytes_for(ec_key)
54
+ der_signature = raw_to_der(signature, n_bytes)
55
+ ec_key.verify(digest_name, der_signature, data)
56
+ rescue ArgumentError
57
+ false
58
+ end
59
+
60
+ private
61
+
62
+ def coerce_key(key)
63
+ case key
64
+ when OpenSSL::PKey::EC then key
65
+ when OpenSSL::PKey::EC::Point
66
+ raise SignatureKeyError,
67
+ "ECDSA requires an EC key, not a bare Point"
68
+ else
69
+ raise SignatureKeyError,
70
+ "ECDSA signature method requires OpenSSL::PKey::EC, " \
71
+ "got #{key.class}"
72
+ end
73
+ end
74
+
75
+ def coordinate_bytes_for(ec_key)
76
+ curve_name = ec_key.group.curve_name
77
+ self.class::CURVE_ORDER_BYTES.fetch(curve_name) do
78
+ raise SignatureError,
79
+ "ECDSA curve #{curve_name.inspect} not supported; " \
80
+ "add it to EcdsaSha::CURVE_ORDER_BYTES"
81
+ end
82
+ end
83
+
84
+ # Convert ASN.1 DER sequence of two INTEGERs to raw r‖s octets.
85
+ def der_to_raw(der, n_bytes)
86
+ seq = OpenSSL::ASN1.decode(der)
87
+ r = seq.value[0].value
88
+ s = seq.value[1].value
89
+ i2osp(r, n_bytes) + i2osp(s, n_bytes)
90
+ end
91
+
92
+ # Convert raw r‖s octets back to ASN.1 DER.
93
+ def raw_to_der(raw, n_bytes)
94
+ raise ArgumentError, "raw signature has wrong size" if raw.bytesize != (2 * n_bytes)
95
+
96
+ r = raw.byteslice(0, n_bytes)
97
+ s = raw.byteslice(n_bytes, n_bytes)
98
+ seq = OpenSSL::ASN1::Sequence.new([
99
+ OpenSSL::ASN1::Integer.new(asn1_integer_value(r)),
100
+ OpenSSL::ASN1::Integer.new(asn1_integer_value(s)),
101
+ ])
102
+ seq.to_der
103
+ end
104
+
105
+ # Integer to octet stream (I2OSP), minimal length parameter.
106
+ def i2osp(value, length)
107
+ n = value.to_i
108
+ raise ArgumentError, "I2OSP: integer too large" if n >= (1 << (8 * length))
109
+
110
+ bytes = Array.new(length, 0)
111
+ (length - 1).downto(0) do |i|
112
+ bytes[i] = n & 0xff
113
+ n >>= 8
114
+ end
115
+ bytes.pack("C*")
116
+ end
117
+
118
+ # Interpret an octet string as a positive integer (OS2IP).
119
+ def asn1_integer_value(octets)
120
+ octets.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ # Enveloped Signature Transform (W3C §6.6.4).
7
+ #
8
+ # Removes the containing ds:Signature element from the node-set so the
9
+ # signature does not include itself in its own digest calculation.
10
+ #
11
+ # Semantics during signing: `signature_element` is nil (the Signature
12
+ # has not been attached to the document yet), so the transform is a
13
+ # no-op — the document contains no Signature to exclude.
14
+ #
15
+ # Semantics during verification: `signature_element` is the Signature
16
+ # element being verified. The transform detaches it (and its
17
+ # descendants) from a deep copy of the input so the canonicalizer
18
+ # walks a tree without the Signature.
19
+ class EnvelopedSignatureTransform < TransformBase
20
+ identifier "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
21
+
22
+ def self.input_type; :nodeset; end
23
+ def self.output_type; :nodeset; end
24
+
25
+ def transform(input)
26
+ return input if @signature_element.nil?
27
+
28
+ # If the signature is not within the input subtree, no-op.
29
+ return input unless signature_within?(input)
30
+
31
+ working_copy = deep_copy(input)
32
+ remove_signature_within(working_copy)
33
+ working_copy
34
+ end
35
+
36
+ private
37
+
38
+ def signature_within?(input)
39
+ !find_signature_element(input).nil?
40
+ end
41
+
42
+ def remove_signature_within(node)
43
+ sig = find_signature_element(node)
44
+ sig&.remove
45
+ end
46
+
47
+ def find_signature_element(node)
48
+ return nil unless node
49
+ return node if signature_element?(node)
50
+
51
+ children = node.children if node.is_a?(::Moxml::Node)
52
+ children&.each do |child|
53
+ found = find_signature_element(child)
54
+ return found if found
55
+ end
56
+ nil
57
+ end
58
+
59
+ def signature_element?(node)
60
+ node.is_a?(::Moxml::Element) &&
61
+ node.name == "Signature" &&
62
+ node.namespace_uri == DSIG_NS
63
+ end
64
+
65
+ def deep_copy(input)
66
+ case input
67
+ when ::Moxml::Document
68
+ @context.parse(input.root.to_xml(indent: 0))
69
+ when ::Moxml::Element
70
+ wrapper = @context.parse("<__wrap__>#{input.to_xml(indent: 0)}</__wrap__>")
71
+ wrapper.root.children.first
72
+ else
73
+ input
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ # Exclusive XML Canonicalization 1.0 (https://www.w3.org/TR/xml-exc-c14n/)
7
+ #
8
+ # Two registered URIs: one omits comments, one includes comments.
9
+ # The with_comments variant is detected from the URI suffix at
10
+ # instantiation time (see CanonicalizationBase#uri_has_comments?).
11
+ class ExcC14n10 < CanonicalizationBase
12
+ identifier "http://www.w3.org/2001/10/xml-exc-c14n#"
13
+ identifier "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
14
+
15
+ private
16
+
17
+ def engine
18
+ ::Moxml::C14n::Exclusive.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # HMAC per RFC 2104, bound to a digest.
9
+ #
10
+ # Per W3C XML Signature §4.4.2 and §6.3.1, if `HMACOutputLength` is
11
+ # specified, the output is truncated to that many bits and the
12
+ # truncation length MUST be at least max(hash_bits / 2, 80) bits.
13
+ class HmacSha < SignatureMethodBase
14
+ PAIRINGS = {
15
+ "http://www.w3.org/2000/09/xmldsig#hmac-sha1" => "SHA1",
16
+ "http://www.w3.org/2001/04/xmldsig-more#hmac-sha224" => "SHA224",
17
+ "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" => "SHA256",
18
+ "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384" => "SHA384",
19
+ "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512" => "SHA512",
20
+ }.freeze
21
+
22
+ HASH_BITS = {
23
+ "SHA1" => 160,
24
+ "SHA224" => 224,
25
+ "SHA256" => 256,
26
+ "SHA384" => 384,
27
+ "SHA512" => 512,
28
+ }.freeze
29
+
30
+ MIN_TRUNCATION_BITS = 80
31
+
32
+ PAIRINGS.each_key { |uri| identifier uri }
33
+
34
+ # parameters: optional { hmac_output_length: Integer } (bits, multiple of 8)
35
+ def initialize(identifier_uri:, parameters: nil)
36
+ super(parameters)
37
+ @identifier_uri = identifier_uri
38
+ @truncation_bits = parameters&.dig(:hmac_output_length)
39
+ validate_truncation! if @truncation_bits
40
+ end
41
+
42
+ def compute_signature(data, key)
43
+ secret = coerce_key(key)
44
+ full = OpenSSL::HMAC.digest(digest_name, secret, data)
45
+ truncate(full)
46
+ end
47
+
48
+ def verify_signature(data, key, signature)
49
+ expected = compute_signature(data, key)
50
+ fixed_comparison(expected, signature)
51
+ end
52
+
53
+ private
54
+
55
+ def digest_name
56
+ PAIRINGS.fetch(@identifier_uri)
57
+ end
58
+
59
+ def hash_bits
60
+ HASH_BITS.fetch(digest_name)
61
+ end
62
+
63
+ def truncate(full_mac)
64
+ return full_mac unless @truncation_bits
65
+
66
+ unless (@truncation_bits % 8).zero?
67
+ raise SignatureError,
68
+ "HMACOutputLength (#{@truncation_bits}) must be a " \
69
+ "multiple of 8"
70
+ end
71
+
72
+ min = [hash_bits / 2, MIN_TRUNCATION_BITS].max
73
+ if @truncation_bits < min
74
+ raise SignatureError,
75
+ "HMACOutputLength (#{@truncation_bits}) below minimum " \
76
+ "of #{min} for #{digest_name}"
77
+ end
78
+
79
+ full_mac.byteslice(0, @truncation_bits / 8)
80
+ end
81
+
82
+ def validate_truncation!
83
+ unless (@truncation_bits % 8).zero?
84
+ raise SignatureError,
85
+ "HMACOutputLength (#{@truncation_bits}) must be a " \
86
+ "multiple of 8"
87
+ end
88
+
89
+ min = [hash_bits / 2, MIN_TRUNCATION_BITS].max
90
+ return if @truncation_bits >= min
91
+
92
+ raise SignatureError,
93
+ "HMACOutputLength (#{@truncation_bits}) below minimum " \
94
+ "of #{min} for #{digest_name}"
95
+ end
96
+
97
+ def coerce_key(key)
98
+ case key
99
+ when String then key
100
+ else
101
+ raise SignatureKeyError,
102
+ "HMAC signature method requires a String secret, " \
103
+ "got #{key.class}"
104
+ end
105
+ end
106
+
107
+ def fixed_comparison(expected, actual)
108
+ return false unless expected.bytesize == actual.bytesize
109
+
110
+ OpenSSL.fixed_length_secure_compare(expected, actual)
111
+ rescue ArgumentError
112
+ false
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ # Canonical XML 1.0 (https://www.w3.org/TR/xml-c14n/)
7
+ #
8
+ # Implemented via the canon-derived C14n::Inclusive10 engine.
9
+ class InclusiveC14n10 < CanonicalizationBase
10
+ identifier "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
11
+ identifier "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
12
+
13
+ private
14
+
15
+ def engine
16
+ ::Moxml::C14n::Inclusive10.new
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ # Canonical XML 1.1 (https://www.w3.org/TR/xml-c14n11/)
7
+ #
8
+ # Currently delegates to Inclusive 1.0. Full 1.1 differences
9
+ # (XML 1.1 line-ending handling, notations, DTD entity refs)
10
+ # are documented in TODO.complete/05-c14n-engine.md.
11
+ class InclusiveC14n11 < CanonicalizationBase
12
+ identifier "http://www.w3.org/2006/12/xml-c14n11"
13
+ identifier "http://www.w3.org/2006/12/xml-c14n11#WithComments"
14
+
15
+ private
16
+
17
+ def engine
18
+ ::Moxml::C14n::Inclusive10.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Moxml
6
+ module Signature
7
+ module Algorithms
8
+ # RSASSA-PKCS1-v1_5 per RFC 3447 §8.2, bound to a digest.
9
+ #
10
+ # One Ruby class registered under five URIs (sha1 / sha224 / sha256 /
11
+ # sha384 / sha512). When instantiated, the class is told which URI was
12
+ # resolved so it can pick the right digest.
13
+ class RsaPkcs1Sha < SignatureMethodBase
14
+ PAIRINGS = {
15
+ "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => "SHA1",
16
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224" => "SHA224",
17
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => "SHA256",
18
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => "SHA384",
19
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => "SHA512",
20
+ }.freeze
21
+
22
+ PAIRINGS.each_key { |uri| identifier uri }
23
+
24
+ def initialize(identifier_uri:, parameters: nil)
25
+ super(nil)
26
+ @identifier_uri = identifier_uri
27
+ @parameters = parameters
28
+ end
29
+
30
+ def compute_signature(data, key)
31
+ rsa_key = coerce_key(key)
32
+ rsa_key.sign(digest_name, data)
33
+ end
34
+
35
+ def verify_signature(data, key, signature)
36
+ rsa_key = coerce_key(key)
37
+ rsa_key.verify(digest_name, signature, data)
38
+ end
39
+
40
+ private
41
+
42
+ def digest_name
43
+ PAIRINGS.fetch(@identifier_uri)
44
+ end
45
+
46
+ def coerce_key(key)
47
+ return key if key.is_a?(OpenSSL::PKey::RSA)
48
+
49
+ raise SignatureKeyError,
50
+ "RSA signature method requires OpenSSL::PKey::RSA, " \
51
+ "got #{key.class}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA1 < DigestBase
7
+ identifier "http://www.w3.org/2000/09/xmldsig#sha1"
8
+ digest_bits 160
9
+ openssl_digest_name "SHA1"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA224 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmldsig-more#sha224"
8
+ digest_bits 224
9
+ openssl_digest_name "SHA224"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA256 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmlenc#sha256"
8
+ digest_bits 256
9
+ openssl_digest_name "SHA256"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA384 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmldsig-more#sha384"
8
+ digest_bits 384
9
+ openssl_digest_name "SHA384"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA512 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmlenc#sha512"
8
+ digest_bits 512
9
+ openssl_digest_name "SHA512"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ # Base class for signature methods and MACs.
7
+ #
8
+ # Subclasses declare an identifier URI per W3C spec §6.4 / §6.3 and
9
+ # implement #compute_signature and #verify_signature.
10
+ #
11
+ # `key` is whatever OpenSSL expects (OpenSSL::PKey::RSA for RSA, a
12
+ # String secret for HMAC, etc.).
13
+ class SignatureMethodBase
14
+ class << self
15
+ attr_reader :identifier_uri, :digest_uri
16
+
17
+ def identifier(uri, digest_uri: nil)
18
+ @identifier_uri = uri
19
+ @digest_uri = digest_uri
20
+ Algorithms.register(:signature_method, uri, self)
21
+ end
22
+ end
23
+
24
+ # Optional constructor for parameterized methods (e.g. HMACOutputLength).
25
+ def initialize(parameters = nil)
26
+ @parameters = parameters
27
+ end
28
+
29
+ def sign(data, key)
30
+ compute_signature(data, key)
31
+ rescue OpenSSL::PKey::PKeyError => e
32
+ raise SigningError.new(
33
+ "signing failed: #{e.class}",
34
+ algorithm: self.class.identifier_uri,
35
+ )
36
+ end
37
+
38
+ def verify(data, key, signature)
39
+ verify_signature(data, key, signature)
40
+ rescue OpenSSL::PKey::PKeyError => e
41
+ raise VerificationError.new(
42
+ "verification raised: #{e.class}",
43
+ )
44
+ end
45
+
46
+ def compute_signature(_data, _key)
47
+ raise NotImplementedError,
48
+ "#{self.class} must implement #compute_signature"
49
+ end
50
+
51
+ def verify_signature(_data, _key, _signature)
52
+ raise NotImplementedError,
53
+ "#{self.class} must implement #verify_signature"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end