moxml 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/README.adoc +64 -0
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +2 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- metadata +111 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Translates a Reference URI to a node (subtree) or octet string ready
|
|
6
|
+
# for the transform pipeline.
|
|
7
|
+
#
|
|
8
|
+
# Only same-document references and bare-octet inputs are handled in
|
|
9
|
+
# this tier. External URI dereferencing is documented in
|
|
10
|
+
# TODO.complete/11.
|
|
11
|
+
class ReferenceResolver
|
|
12
|
+
attr_reader :context, :document
|
|
13
|
+
|
|
14
|
+
def initialize(context:, document:)
|
|
15
|
+
@context = context
|
|
16
|
+
@document = document
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns:
|
|
20
|
+
# - Moxml::Node for same-document references (subtree apex)
|
|
21
|
+
# - String (octets) for octet inputs
|
|
22
|
+
def resolve(uri)
|
|
23
|
+
case uri.to_s
|
|
24
|
+
when ""
|
|
25
|
+
document.root
|
|
26
|
+
when /\A#/
|
|
27
|
+
resolve_fragment(uri[1..])
|
|
28
|
+
else
|
|
29
|
+
uri.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def resolve_fragment(fragment)
|
|
36
|
+
case fragment
|
|
37
|
+
when /\Axpointer\(\/\)\z/
|
|
38
|
+
document.root
|
|
39
|
+
when /\Axpointer\(id\(['"]([^'"]+)['"]\)\)\z/
|
|
40
|
+
find_by_id(::Regexp.last_match(1))
|
|
41
|
+
else
|
|
42
|
+
find_by_id(fragment)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_by_id(id)
|
|
47
|
+
candidate = search_by_xml_id(id) || search_by_attribute_id(id)
|
|
48
|
+
unless candidate
|
|
49
|
+
raise MalformedSignatureError.new(
|
|
50
|
+
"Reference URI ##{id.inspect} does not resolve to any element",
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
candidate
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def search_by_xml_id(id)
|
|
57
|
+
result = document.at_xpath("//*[@xml:id='#{xpath_escape(id)}']")
|
|
58
|
+
return nil unless result
|
|
59
|
+
return nil unless result.is_a?(::Moxml::Element)
|
|
60
|
+
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def search_by_attribute_id(id)
|
|
65
|
+
matches = document
|
|
66
|
+
.xpath("//*[@Id='#{xpath_escape(id)}'] | " \
|
|
67
|
+
"//*[@ID='#{xpath_escape(id)}']")
|
|
68
|
+
.grep(::Moxml::Element)
|
|
69
|
+
return nil if matches.empty?
|
|
70
|
+
|
|
71
|
+
matches.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def xpath_escape(value)
|
|
75
|
+
value.to_s.gsub("'", "''")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Per-reference digest comparison result. `valid?` is true iff the
|
|
6
|
+
# freshly-computed digest matches the expected DigestValue.
|
|
7
|
+
class ReferenceResult
|
|
8
|
+
attr_reader :uri, :digest_method, :expected, :computed
|
|
9
|
+
|
|
10
|
+
def initialize(uri:, digest_method:, expected:, computed:)
|
|
11
|
+
@uri = uri
|
|
12
|
+
@digest_method = digest_method
|
|
13
|
+
@expected = expected
|
|
14
|
+
@computed = computed
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid?
|
|
18
|
+
expected == computed
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
# Translates a Model::Signature into a Moxml::Document.
|
|
8
|
+
#
|
|
9
|
+
# All XML construction goes through moxml primitives
|
|
10
|
+
# (create_element, []=, add_child, create_text). No string templates,
|
|
11
|
+
# no manual attribute escaping — moxml handles entity encoding
|
|
12
|
+
# correctly per the XML spec.
|
|
13
|
+
class Serializer
|
|
14
|
+
DS_PREFIX = "ds"
|
|
15
|
+
|
|
16
|
+
attr_reader :context
|
|
17
|
+
|
|
18
|
+
def initialize(context:)
|
|
19
|
+
@context = context
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serialize(signature)
|
|
23
|
+
doc = context.create_document
|
|
24
|
+
root = create_namespaced_element("Signature", doc)
|
|
25
|
+
root["Id"] = signature.id if signature.id
|
|
26
|
+
doc.root = root
|
|
27
|
+
|
|
28
|
+
if signature.signed_info
|
|
29
|
+
root.add_child(build_signed_info(signature.signed_info, doc))
|
|
30
|
+
end
|
|
31
|
+
if signature.signature_value
|
|
32
|
+
root.add_child(build_signature_value(signature.signature_value, doc))
|
|
33
|
+
end
|
|
34
|
+
if signature.key_info
|
|
35
|
+
root.add_child(build_key_info(signature.key_info, doc))
|
|
36
|
+
end
|
|
37
|
+
signature.objects.each do |obj|
|
|
38
|
+
root.add_child(build_object(obj, doc))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
doc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Produces a standalone ds:SignedInfo document. Used by the Signer
|
|
45
|
+
# when canonicalizing SignedInfo in isolation.
|
|
46
|
+
def serialize_signed_info(signed_info)
|
|
47
|
+
doc = context.create_document
|
|
48
|
+
root = create_namespaced_element("SignedInfo", doc)
|
|
49
|
+
root["Id"] = signed_info.id if signed_info.id
|
|
50
|
+
doc.root = root
|
|
51
|
+
populate_signed_info(root, signed_info, doc)
|
|
52
|
+
doc
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_signed_info(signed_info, doc)
|
|
58
|
+
el = create_namespaced_element("SignedInfo", doc)
|
|
59
|
+
el["Id"] = signed_info.id if signed_info.id
|
|
60
|
+
populate_signed_info(el, signed_info, doc)
|
|
61
|
+
el
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def populate_signed_info(root, signed_info, doc)
|
|
65
|
+
if signed_info.canonicalization_method
|
|
66
|
+
root.add_child(
|
|
67
|
+
build_algorithm_method("CanonicalizationMethod",
|
|
68
|
+
signed_info.canonicalization_method, doc),
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
if signed_info.signature_method
|
|
72
|
+
root.add_child(
|
|
73
|
+
build_algorithm_method("SignatureMethod",
|
|
74
|
+
signed_info.signature_method, doc),
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
signed_info.references.each do |ref|
|
|
78
|
+
root.add_child(build_reference(ref, doc))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_algorithm_method(name, method, doc)
|
|
83
|
+
el = create_namespaced_element(name, doc)
|
|
84
|
+
el["Algorithm"] = method.algorithm
|
|
85
|
+
if method.parameters.is_a?(Hash) && method.parameters[:hmac_output_length]
|
|
86
|
+
len = create_namespaced_element("HMACOutputLength", doc)
|
|
87
|
+
len.add_child(doc.create_text(method.parameters[:hmac_output_length].to_s))
|
|
88
|
+
el.add_child(len)
|
|
89
|
+
end
|
|
90
|
+
el
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_reference(reference, doc)
|
|
94
|
+
el = create_namespaced_element("Reference", doc)
|
|
95
|
+
el["Id"] = reference.id if reference.id
|
|
96
|
+
el["URI"] = reference.uri if reference.uri
|
|
97
|
+
el["Type"] = reference.type if reference.type
|
|
98
|
+
|
|
99
|
+
if reference.transforms && !reference.transforms.empty?
|
|
100
|
+
tf = create_namespaced_element("Transforms", doc)
|
|
101
|
+
reference.transforms.each do |transform|
|
|
102
|
+
tr = create_namespaced_element("Transform", doc)
|
|
103
|
+
tr["Algorithm"] = transform.algorithm
|
|
104
|
+
tf.add_child(tr)
|
|
105
|
+
end
|
|
106
|
+
el.add_child(tf)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if reference.digest_method
|
|
110
|
+
dm = create_namespaced_element("DigestMethod", doc)
|
|
111
|
+
dm["Algorithm"] = reference.digest_method.algorithm
|
|
112
|
+
el.add_child(dm)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
dv = create_namespaced_element("DigestValue", doc)
|
|
116
|
+
dv.add_child(doc.create_text(reference.digest_value || ""))
|
|
117
|
+
el.add_child(dv)
|
|
118
|
+
el
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_signature_value(signature_value, doc)
|
|
122
|
+
el = create_namespaced_element("SignatureValue", doc)
|
|
123
|
+
el["Id"] = signature_value.id if signature_value.id
|
|
124
|
+
if signature_value.value
|
|
125
|
+
el.add_child(doc.create_text(Base64.strict_encode64(signature_value.value)))
|
|
126
|
+
end
|
|
127
|
+
el
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_key_info(key_info, doc)
|
|
131
|
+
el = create_namespaced_element("KeyInfo", doc)
|
|
132
|
+
el["Id"] = key_info.id if key_info.id
|
|
133
|
+
if key_info.key_name
|
|
134
|
+
kn = create_namespaced_element("KeyName", doc)
|
|
135
|
+
kn.add_child(doc.create_text(key_info.key_name))
|
|
136
|
+
el.add_child(kn)
|
|
137
|
+
end
|
|
138
|
+
key_info.raw_elements.each { |raw| el.add_child(raw.dup) }
|
|
139
|
+
el
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def build_object(obj, doc)
|
|
143
|
+
el = create_namespaced_element("Object", doc)
|
|
144
|
+
el["Id"] = obj.id if obj.id
|
|
145
|
+
el["MimeType"] = obj.mime_type if obj.mime_type
|
|
146
|
+
el["Encoding"] = obj.encoding if obj.encoding
|
|
147
|
+
# Payload content (Manifest, SignatureProperties, XAdES
|
|
148
|
+
# QualifyingProperties) comes in later tiers.
|
|
149
|
+
el
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Create an element in the xmldsig# namespace with the conventional
|
|
153
|
+
# `ds` prefix. The namespace declaration is added once on the
|
|
154
|
+
# element itself; descendants inherit it.
|
|
155
|
+
def create_namespaced_element(local_name, doc)
|
|
156
|
+
el = doc.create_element(local_name)
|
|
157
|
+
el.add_namespace(DS_PREFIX, DSIG_NS)
|
|
158
|
+
el.namespace = { DS_PREFIX => DSIG_NS }
|
|
159
|
+
el
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Reference generation + SignatureValue computation per spec §3.1.
|
|
6
|
+
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. For each Reference in signed_info.references:
|
|
9
|
+
# a. Resolve URI (same-document or octet).
|
|
10
|
+
# b. Apply each Transform in order.
|
|
11
|
+
# c. Compute digest via DigestMethod.
|
|
12
|
+
# d. Store base64 digest on Reference#digest_value.
|
|
13
|
+
# 2. Serialize SignedInfo.
|
|
14
|
+
# 3. Canonicalize per CanonicalizationMethod.
|
|
15
|
+
# 4. Sign via SignatureMethod#sign(canonical_octets, key).
|
|
16
|
+
# 5. Set signature.signature_value.
|
|
17
|
+
class Signer
|
|
18
|
+
attr_reader :context, :signature, :document, :key
|
|
19
|
+
|
|
20
|
+
def initialize(context:, signature:, document:, key:)
|
|
21
|
+
@context = context
|
|
22
|
+
@signature = signature
|
|
23
|
+
@document = document
|
|
24
|
+
@key = key
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sign
|
|
28
|
+
Algorithms.load_builtins!
|
|
29
|
+
|
|
30
|
+
signature.signed_info.references.each do |ref|
|
|
31
|
+
digest = compute_reference_digest(ref)
|
|
32
|
+
ref.digest_value = digest
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
signature.signature_value = Model::SignatureValue.new(
|
|
36
|
+
value: compute_signature_value,
|
|
37
|
+
)
|
|
38
|
+
signature
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def compute_reference_digest(reference)
|
|
44
|
+
resolver = ReferenceResolver.new(context: context, document: document)
|
|
45
|
+
input = resolver.resolve(reference.uri)
|
|
46
|
+
pipeline = TransformPipeline.new(context: context, signature_element: nil)
|
|
47
|
+
transformed = pipeline.apply(input, reference.transforms)
|
|
48
|
+
canonical = pipeline.to_octets(transformed, reference)
|
|
49
|
+
digest_algo = Algorithms.lookup(:digest, reference.digest_method.algorithm).new
|
|
50
|
+
digest_algo.digest_base64(canonical)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def compute_signature_value
|
|
54
|
+
signed_info = signature.signed_info
|
|
55
|
+
serializer = Serializer.new(context: context)
|
|
56
|
+
signed_info_doc = serializer.serialize_signed_info(signed_info)
|
|
57
|
+
|
|
58
|
+
c14n_uri = signed_info.canonicalization_method.algorithm
|
|
59
|
+
c14n_klass = Algorithms.lookup(:canonicalization, c14n_uri)
|
|
60
|
+
c14n = c14n_klass.new(identifier_uri: c14n_uri)
|
|
61
|
+
canonical_octets = c14n.canonicalize(signed_info_doc.root)
|
|
62
|
+
|
|
63
|
+
sm_uri = signed_info.signature_method.algorithm
|
|
64
|
+
sm_klass = Algorithms.lookup(:signature_method, sm_uri)
|
|
65
|
+
sm = sm_klass.new(
|
|
66
|
+
identifier_uri: sm_uri,
|
|
67
|
+
parameters: signed_info.signature_method.parameters,
|
|
68
|
+
)
|
|
69
|
+
sm.sign(canonical_octets, key)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Verification outcome for a single Signature element. Carries the
|
|
6
|
+
# cryptographic-result boolean, per-reference results, and (when
|
|
7
|
+
# applicable) the error that caused failure.
|
|
8
|
+
class SingleVerificationResult
|
|
9
|
+
attr_reader :signature_id, :references, :error
|
|
10
|
+
|
|
11
|
+
def initialize(signature_id:, signature_valid:, references:, error: nil)
|
|
12
|
+
@signature_id = signature_id
|
|
13
|
+
@signature_valid = signature_valid
|
|
14
|
+
@references = references
|
|
15
|
+
@error = error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signature_valid?
|
|
19
|
+
@signature_valid
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def valid?
|
|
23
|
+
@signature_valid && references.all?(&:valid?)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failing_references
|
|
27
|
+
references.reject(&:valid?)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Shared transform-pipeline logic used by both Signer (signing flow,
|
|
6
|
+
# signature_element: nil) and Verifier (verification flow, with the
|
|
7
|
+
# containing Signature element so the Enveloped Signature transform
|
|
8
|
+
# can remove it from the digest input).
|
|
9
|
+
#
|
|
10
|
+
# Each Transform in the chain is looked up via Algorithms.lookup(:transform, uri),
|
|
11
|
+
# with a fallback to Algorithms.lookup(:canonicalization, uri) per
|
|
12
|
+
# spec §6.6.1 (any canonicalization algorithm can be used as a transform).
|
|
13
|
+
#
|
|
14
|
+
# Type coercion matches spec §4.4.3.2 reference processing model:
|
|
15
|
+
# - octets → nodeset: parse as XML
|
|
16
|
+
# - nodeset → octets: apply inclusive C14N 1.0
|
|
17
|
+
class TransformPipeline
|
|
18
|
+
attr_reader :context, :signature_element
|
|
19
|
+
|
|
20
|
+
def initialize(context:, signature_element: nil)
|
|
21
|
+
@context = context
|
|
22
|
+
@signature_element = signature_element
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def apply(input, transforms_model)
|
|
26
|
+
current = input
|
|
27
|
+
current_type = type_of(current)
|
|
28
|
+
transforms = transforms_from(transforms_model)
|
|
29
|
+
|
|
30
|
+
transforms.each do |transform_model|
|
|
31
|
+
algo_class = lookup(transform_model.algorithm)
|
|
32
|
+
transform = algo_class.new(
|
|
33
|
+
parameters: transform_model.parameters,
|
|
34
|
+
context: context,
|
|
35
|
+
signature_element: signature_element,
|
|
36
|
+
)
|
|
37
|
+
current_type, current = coerce(current, current_type,
|
|
38
|
+
algo_class.input_type)
|
|
39
|
+
current = transform.transform(current)
|
|
40
|
+
current_type = algo_class.output_type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Final step: convert the pipeline output (which may still be a node)
|
|
47
|
+
# to octets suitable for digesting. Uses inclusive C14N 1.0 as the
|
|
48
|
+
# default mapping per spec §4.4.3.2.
|
|
49
|
+
def to_octets(value, reference = nil)
|
|
50
|
+
return value if value.is_a?(String)
|
|
51
|
+
|
|
52
|
+
c14n_uri = canonicalization_from(reference) || DEFAULT_OCTET_C14N
|
|
53
|
+
klass = Algorithms.lookup(:canonicalization, c14n_uri)
|
|
54
|
+
klass.new(identifier_uri: c14n_uri).canonicalize(value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
DEFAULT_OCTET_C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
|
|
58
|
+
|
|
59
|
+
private_constant :DEFAULT_OCTET_C14N
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def transforms_from(transforms_model)
|
|
64
|
+
return [] unless transforms_model
|
|
65
|
+
|
|
66
|
+
transforms_model.transforms
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def lookup(uri)
|
|
70
|
+
Algorithms.lookup(:transform, uri)
|
|
71
|
+
rescue UnknownAlgorithm
|
|
72
|
+
Algorithms.lookup(:canonicalization, uri)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def type_of(value)
|
|
76
|
+
case value
|
|
77
|
+
when ::Moxml::Node, Array then :nodeset
|
|
78
|
+
else :octets
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def coerce(input, from, to)
|
|
83
|
+
return [to, input] if from == to
|
|
84
|
+
|
|
85
|
+
case [from, to]
|
|
86
|
+
when %i[octets nodeset]
|
|
87
|
+
parsed = context.parse(input.to_s)
|
|
88
|
+
[:nodeset, parsed.root]
|
|
89
|
+
when %i[nodeset octets]
|
|
90
|
+
octets = C14n::Inclusive10.new.canonicalize(input)
|
|
91
|
+
[:octets, octets]
|
|
92
|
+
else
|
|
93
|
+
raise TransformError, "cannot coerce #{from} to #{to}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def canonicalization_from(reference)
|
|
98
|
+
return nil unless reference&.transforms
|
|
99
|
+
|
|
100
|
+
reference.transforms.transforms.reverse_each.find do |t|
|
|
101
|
+
Algorithms.registered?(:canonicalization, t.algorithm)
|
|
102
|
+
end&.algorithm
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Aggregated verification result for a document. A document may
|
|
6
|
+
# contain multiple Signature elements; this result wraps them all.
|
|
7
|
+
class VerificationResult
|
|
8
|
+
attr_reader :results
|
|
9
|
+
|
|
10
|
+
def initialize(results:)
|
|
11
|
+
@results = results
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?
|
|
15
|
+
results.all?(&:valid?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signature_count
|
|
19
|
+
results.size
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failing
|
|
23
|
+
results.reject(&:valid?)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Core Validation per spec §3.2, with the safe ordering prescribed by
|
|
6
|
+
# Best Practice 1: verify SignatureValue BEFORE running any reference
|
|
7
|
+
# transforms (which could be hostile).
|
|
8
|
+
class Verifier
|
|
9
|
+
attr_reader :context, :document, :key, :key_map
|
|
10
|
+
|
|
11
|
+
def initialize(context:, document:, key: nil, key_map: {}, **_options)
|
|
12
|
+
@context = context
|
|
13
|
+
@document = document
|
|
14
|
+
@key = key
|
|
15
|
+
@key_map = key_map || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify
|
|
19
|
+
Algorithms.load_builtins!
|
|
20
|
+
signatures = find_signatures
|
|
21
|
+
|
|
22
|
+
results = signatures.map { |sig_elem| verify_one(sig_elem) }
|
|
23
|
+
VerificationResult.new(results: results)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def find_signatures
|
|
29
|
+
document.xpath("//ds:Signature", "ds" => DSIG_NS)
|
|
30
|
+
.grep(::Moxml::Element)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def verify_one(signature_element)
|
|
34
|
+
signature = Parser.new(context: context).parse(signature_element)
|
|
35
|
+
signature_value_ok, error = verify_signature_value(signature, signature_element)
|
|
36
|
+
reference_results = if signature_value_ok
|
|
37
|
+
verify_references(signature, signature_element)
|
|
38
|
+
else
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
SingleVerificationResult.new(
|
|
43
|
+
signature_id: signature.id,
|
|
44
|
+
signature_valid: signature_value_ok,
|
|
45
|
+
references: reference_results,
|
|
46
|
+
error: error,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns [Boolean, ErrorOrNil] so the caller can attach the error
|
|
51
|
+
# to the result for debugging. The error is never raised — Best
|
|
52
|
+
# Practice 1 says authenticate first; surfacing why auth failed is
|
|
53
|
+
# an application concern, not a panic.
|
|
54
|
+
def verify_signature_value(signature, signature_element)
|
|
55
|
+
return [false, nil] if signature.signature_value.nil?
|
|
56
|
+
return [false, nil] if signature.signature_value.value.nil?
|
|
57
|
+
|
|
58
|
+
signed_info_elem = signature_element.at_xpath(
|
|
59
|
+
"./ds:SignedInfo", "ds" => DSIG_NS
|
|
60
|
+
)
|
|
61
|
+
return [false, nil] if signed_info_elem.nil?
|
|
62
|
+
|
|
63
|
+
c14n_uri = signature.signed_info.canonicalization_method&.algorithm
|
|
64
|
+
return [false, nil] if c14n_uri.nil?
|
|
65
|
+
|
|
66
|
+
canonical = canonicalize_signed_info(c14n_uri, signed_info_elem)
|
|
67
|
+
sm_uri = signature.signed_info.signature_method.algorithm
|
|
68
|
+
sm = Algorithms.lookup(:signature_method, sm_uri).new(
|
|
69
|
+
identifier_uri: sm_uri,
|
|
70
|
+
parameters: signature.signed_info.signature_method.parameters,
|
|
71
|
+
)
|
|
72
|
+
verification_key = resolve_key(signature)
|
|
73
|
+
return [false, nil] if verification_key.nil?
|
|
74
|
+
|
|
75
|
+
ok = sm.verify(canonical, verification_key, signature.signature_value.value)
|
|
76
|
+
[ok, nil]
|
|
77
|
+
rescue VerificationError, UnknownAlgorithm => e
|
|
78
|
+
[false, e]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Canonicalize the original SignedInfo element as it appears in the
|
|
82
|
+
# document. Re-serializing the parsed model would change namespace
|
|
83
|
+
# prefixes (e.g. SignedInfo → ds:SignedInfo) and break byte-exact
|
|
84
|
+
# verification.
|
|
85
|
+
def canonicalize_signed_info(c14n_uri, signed_info_elem)
|
|
86
|
+
Algorithms.lookup(:canonicalization, c14n_uri)
|
|
87
|
+
.new(identifier_uri: c14n_uri)
|
|
88
|
+
.canonicalize(signed_info_elem)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_key(signature)
|
|
92
|
+
return key if key
|
|
93
|
+
return nil unless signature.key_info
|
|
94
|
+
|
|
95
|
+
KeyExtractor.new(key_map: key_map).extract(signature.key_info)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def verify_references(signature, signature_element)
|
|
99
|
+
signature.signed_info.references.map do |ref|
|
|
100
|
+
verify_reference(ref, signature_element)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def verify_reference(reference, signature_element)
|
|
105
|
+
resolver = ReferenceResolver.new(context: context, document: document)
|
|
106
|
+
input = resolver.resolve(reference.uri)
|
|
107
|
+
pipeline = TransformPipeline.new(
|
|
108
|
+
context: context,
|
|
109
|
+
signature_element: signature_element,
|
|
110
|
+
)
|
|
111
|
+
transformed = pipeline.apply(input, reference.transforms)
|
|
112
|
+
canonical = pipeline.to_octets(transformed, reference)
|
|
113
|
+
|
|
114
|
+
digest_algo = Algorithms.lookup(
|
|
115
|
+
:digest, reference.digest_method.algorithm
|
|
116
|
+
).new
|
|
117
|
+
computed = digest_algo.digest_base64(canonical)
|
|
118
|
+
expected = (reference.digest_value || "").strip
|
|
119
|
+
|
|
120
|
+
ReferenceResult.new(
|
|
121
|
+
uri: reference.uri,
|
|
122
|
+
digest_method: reference.digest_method.algorithm,
|
|
123
|
+
expected: expected,
|
|
124
|
+
computed: computed,
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|