moxml 0.1.25 → 0.2.0
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/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- 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 +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -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/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- 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 +28 -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/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- 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/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -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/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -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/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +130 -2
|
@@ -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,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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Algorithms
|
|
6
|
+
# Base class for XML Signature transforms.
|
|
7
|
+
#
|
|
8
|
+
# Each transform declares input and output types (:octets or :nodeset)
|
|
9
|
+
# plus an identifier URI. The reference resolver chains them with
|
|
10
|
+
# default type conversion when types mismatch (octets → nodeset via
|
|
11
|
+
# XML parse; nodeset → octets via inclusive C14N 1.0).
|
|
12
|
+
class TransformBase
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :identifier_uri
|
|
15
|
+
|
|
16
|
+
def identifier(uri)
|
|
17
|
+
@identifier_uri = uri
|
|
18
|
+
Algorithms.register(:transform, uri, self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def input_type; :octets; end
|
|
22
|
+
def output_type; :octets; end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# `parameters`: optional hash parsed from the ds:Transform element
|
|
26
|
+
# (e.g. { xpaths: [...] } for the XPath Filter transform).
|
|
27
|
+
def initialize(parameters: nil, context: nil, signature_element: nil)
|
|
28
|
+
@parameters = parameters || {}
|
|
29
|
+
@context = context
|
|
30
|
+
@signature_element = signature_element
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def transform(_input)
|
|
34
|
+
raise NotImplementedError,
|
|
35
|
+
"#{self.class} must implement #transform"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Open-closed registry of W3C XML Signature algorithms keyed by URI.
|
|
6
|
+
#
|
|
7
|
+
# Adding a new algorithm:
|
|
8
|
+
# 1. Subclass the relevant base (DigestBase, SignatureMethodBase, etc.)
|
|
9
|
+
# 2. Declare `identifier "http://..."` on the subclass.
|
|
10
|
+
# 3. Add an autoload entry below and a reference in `load_builtins!`.
|
|
11
|
+
#
|
|
12
|
+
# No edits to existing classes are required to add a new algorithm.
|
|
13
|
+
module Algorithms
|
|
14
|
+
CATEGORIES = %i[digest signature_method canonicalization transform].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def registry
|
|
18
|
+
@registry ||= CATEGORIES.to_h { |c| [c, {}] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register(category, uri, klass)
|
|
22
|
+
validate_category!(category)
|
|
23
|
+
registry[category][uri] = klass
|
|
24
|
+
klass
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def lookup(category, uri)
|
|
28
|
+
load_builtins! unless @builtins_loaded
|
|
29
|
+
registry[category][uri] ||
|
|
30
|
+
raise(UnknownAlgorithm.new(category, uri))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def registered?(category, uri)
|
|
34
|
+
load_builtins! unless @builtins_loaded
|
|
35
|
+
registry[category].key?(uri)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def [](category)
|
|
39
|
+
validate_category!(category)
|
|
40
|
+
registry[category]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Forces autoload of every built-in algorithm class so that
|
|
44
|
+
# self-registration runs. Pure autoload — no `require` calls.
|
|
45
|
+
def load_builtins!
|
|
46
|
+
return if @builtins_loaded
|
|
47
|
+
|
|
48
|
+
# Digests
|
|
49
|
+
SHA1
|
|
50
|
+
SHA224
|
|
51
|
+
SHA256
|
|
52
|
+
SHA384
|
|
53
|
+
SHA512
|
|
54
|
+
# Signature methods
|
|
55
|
+
RsaPkcs1Sha
|
|
56
|
+
HmacSha
|
|
57
|
+
EcdsaSha
|
|
58
|
+
DsaSha
|
|
59
|
+
# Canonicalization
|
|
60
|
+
ExcC14n10
|
|
61
|
+
InclusiveC14n10
|
|
62
|
+
InclusiveC14n11
|
|
63
|
+
# Transforms
|
|
64
|
+
Base64Transform
|
|
65
|
+
EnvelopedSignatureTransform
|
|
66
|
+
|
|
67
|
+
@builtins_loaded = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def validate_category!(category)
|
|
73
|
+
return if CATEGORIES.include?(category)
|
|
74
|
+
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"unknown algorithm category #{category.inspect}; " \
|
|
77
|
+
"expected one of #{CATEGORIES.inspect}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Base classes — interfaces only.
|
|
82
|
+
autoload :DigestBase, "moxml/signature/algorithms/digest_base"
|
|
83
|
+
autoload :SignatureMethodBase,
|
|
84
|
+
"moxml/signature/algorithms/signature_method_base"
|
|
85
|
+
autoload :CanonicalizationBase,
|
|
86
|
+
"moxml/signature/algorithms/canonicalization_base"
|
|
87
|
+
autoload :TransformBase, "moxml/signature/algorithms/transform_base"
|
|
88
|
+
|
|
89
|
+
# Digests
|
|
90
|
+
autoload :SHA1, "moxml/signature/algorithms/sha1"
|
|
91
|
+
autoload :SHA224, "moxml/signature/algorithms/sha224"
|
|
92
|
+
autoload :SHA256, "moxml/signature/algorithms/sha256"
|
|
93
|
+
autoload :SHA384, "moxml/signature/algorithms/sha384"
|
|
94
|
+
autoload :SHA512, "moxml/signature/algorithms/sha512"
|
|
95
|
+
|
|
96
|
+
# Signature methods
|
|
97
|
+
autoload :RsaPkcs1Sha, "moxml/signature/algorithms/rsa_pkcs1_sha"
|
|
98
|
+
autoload :HmacSha, "moxml/signature/algorithms/hmac_sha"
|
|
99
|
+
autoload :EcdsaSha, "moxml/signature/algorithms/ecdsa_sha"
|
|
100
|
+
autoload :DsaSha, "moxml/signature/algorithms/dsa_sha"
|
|
101
|
+
|
|
102
|
+
# Canonicalization (delegates to Moxml::C14n engine)
|
|
103
|
+
autoload :ExcC14n10, "moxml/signature/algorithms/exc_c14n_10"
|
|
104
|
+
autoload :InclusiveC14n10, "moxml/signature/algorithms/inclusive_c14n_10"
|
|
105
|
+
autoload :InclusiveC14n11, "moxml/signature/algorithms/inclusive_c14n_11"
|
|
106
|
+
|
|
107
|
+
# Transforms
|
|
108
|
+
autoload :Base64Transform, "moxml/signature/algorithms/base64_transform"
|
|
109
|
+
autoload :EnvelopedSignatureTransform,
|
|
110
|
+
"moxml/signature/algorithms/enveloped_signature_transform"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|