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,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
# Translates a Moxml::Document containing ds:Signature into a Model::Signature.
|
|
8
|
+
#
|
|
9
|
+
# Robust against namespace prefix variations (ds:, dsig:, default ns).
|
|
10
|
+
class Parser
|
|
11
|
+
DS = { "ds" => DSIG_NS }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :context
|
|
14
|
+
|
|
15
|
+
def initialize(context:)
|
|
16
|
+
@context = context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# `signature_element`: a Moxml::Element whose name is Signature in the
|
|
20
|
+
# xmldsig# namespace.
|
|
21
|
+
def parse(signature_element)
|
|
22
|
+
Model::Signature.new(
|
|
23
|
+
id: signature_element["Id"],
|
|
24
|
+
signed_info: parse_signed_info(at_child(signature_element, "SignedInfo")),
|
|
25
|
+
signature_value: parse_signature_value(at_child(signature_element, "SignatureValue")),
|
|
26
|
+
key_info: parse_key_info(at_child(signature_element, "KeyInfo")),
|
|
27
|
+
objects: [],
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_signed_info(elem)
|
|
34
|
+
return nil unless elem
|
|
35
|
+
|
|
36
|
+
Model::SignedInfo.new(
|
|
37
|
+
id: elem["Id"],
|
|
38
|
+
canonicalization_method: parse_algorithm_method(at_child(elem, "CanonicalizationMethod")),
|
|
39
|
+
signature_method: parse_algorithm_method(at_child(elem, "SignatureMethod")),
|
|
40
|
+
references: at_children(elem, "Reference").map { |r| parse_reference(r) },
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_algorithm_method(elem)
|
|
45
|
+
return nil unless elem
|
|
46
|
+
|
|
47
|
+
parameters = {}
|
|
48
|
+
hmac_len = at_child(elem, "HMACOutputLength")
|
|
49
|
+
if hmac_len
|
|
50
|
+
length = begin
|
|
51
|
+
Integer(hmac_len.text.strip)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
parameters[:hmac_output_length] = length if length
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Model::AlgorithmMethod.new(
|
|
59
|
+
algorithm: elem["Algorithm"],
|
|
60
|
+
parameters: parameters,
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parse_reference(elem)
|
|
65
|
+
return nil unless elem
|
|
66
|
+
|
|
67
|
+
transforms_elem = at_child(elem, "Transforms")
|
|
68
|
+
Model::Reference.new(
|
|
69
|
+
id: elem["Id"],
|
|
70
|
+
uri: elem["URI"],
|
|
71
|
+
type: elem["Type"],
|
|
72
|
+
transforms: parse_transforms(transforms_elem),
|
|
73
|
+
digest_method: parse_digest_method(at_child(elem, "DigestMethod")),
|
|
74
|
+
digest_value: at_child(elem, "DigestValue")&.text || "",
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_transforms(elem)
|
|
79
|
+
return nil unless elem
|
|
80
|
+
|
|
81
|
+
transforms = at_children(elem, "Transform").map do |t|
|
|
82
|
+
xpath_children = at_children(t, "XPath").map(&:text)
|
|
83
|
+
Model::Transform.new(
|
|
84
|
+
algorithm: t["Algorithm"],
|
|
85
|
+
parameters: xpath_children.empty? ? {} : { xpaths: xpath_children },
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
Model::Transforms.new(transforms: transforms)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_digest_method(elem)
|
|
92
|
+
return nil unless elem
|
|
93
|
+
|
|
94
|
+
Model::DigestMethod.new(algorithm: elem["Algorithm"])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_signature_value(elem)
|
|
98
|
+
return nil unless elem
|
|
99
|
+
|
|
100
|
+
text = (elem.text || "").gsub(/\s+/, "")
|
|
101
|
+
value = text.empty? ? nil : Base64.strict_decode64(text)
|
|
102
|
+
Model::SignatureValue.new(id: elem["Id"], value: value)
|
|
103
|
+
rescue ArgumentError => e
|
|
104
|
+
raise MalformedSignatureError.new(
|
|
105
|
+
"SignatureValue is not valid base64: #{e.message}",
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_key_info(elem)
|
|
110
|
+
return nil unless elem
|
|
111
|
+
|
|
112
|
+
key_name_el = at_child(elem, "KeyName")
|
|
113
|
+
x509_data_el = at_child(elem, "X509Data")
|
|
114
|
+
key_value_el = at_child(elem, "KeyValue")
|
|
115
|
+
|
|
116
|
+
Model::KeyInfo.new(
|
|
117
|
+
id: elem["Id"],
|
|
118
|
+
key_name: key_name_el&.text,
|
|
119
|
+
x509_data: parse_x509_data(x509_data_el),
|
|
120
|
+
key_value: parse_key_value(key_value_el),
|
|
121
|
+
raw_elements: raw_key_info_children(elem),
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_x509_data(elem)
|
|
126
|
+
return nil unless elem
|
|
127
|
+
|
|
128
|
+
certificates = at_children(elem, "X509Certificate").map do |cert_el|
|
|
129
|
+
strip_base64(cert_el.text)
|
|
130
|
+
end
|
|
131
|
+
issuer_serial_el = at_child(elem, "X509IssuerSerial")
|
|
132
|
+
subject_name_el = at_child(elem, "X509SubjectName")
|
|
133
|
+
ski_el = at_child(elem, "X509SKI")
|
|
134
|
+
crl_els = at_children(elem, "X509CRL")
|
|
135
|
+
# dsig11:X509Digest is in a different namespace; look it up loosely.
|
|
136
|
+
digest_els = elem.children.select do |c|
|
|
137
|
+
c.is_a?(::Moxml::Element) &&
|
|
138
|
+
c.name == "X509Digest" &&
|
|
139
|
+
c.namespace_uri == DSIG11_NS
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Model::Key::X509Data.new(
|
|
143
|
+
issuer_serial: parse_x509_issuer_serial(issuer_serial_el),
|
|
144
|
+
subject_name: subject_name_el&.text,
|
|
145
|
+
subject_key_id: strip_base64(ski_el&.text),
|
|
146
|
+
certificates: certificates,
|
|
147
|
+
crls: crl_els.map { |e| strip_base64(e.text) },
|
|
148
|
+
digests: digest_els.map { |e| parse_x509_digest(e) },
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_x509_issuer_serial(elem)
|
|
153
|
+
return nil unless elem
|
|
154
|
+
|
|
155
|
+
issuer = at_child(elem, "X509IssuerName")
|
|
156
|
+
serial = at_child(elem, "X509SerialNumber")
|
|
157
|
+
return nil unless issuer && serial
|
|
158
|
+
|
|
159
|
+
Model::Key::X509IssuerSerial.new(
|
|
160
|
+
issuer_name: issuer.text,
|
|
161
|
+
serial_number: serial.text,
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_x509_digest(elem)
|
|
166
|
+
return nil unless elem
|
|
167
|
+
|
|
168
|
+
Model::Key::X509Digest.new(
|
|
169
|
+
algorithm: elem["Algorithm"],
|
|
170
|
+
digest: strip_base64(elem.text),
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_key_value(elem)
|
|
175
|
+
return nil unless elem
|
|
176
|
+
|
|
177
|
+
rsa_el = at_child(elem, "RSAKeyValue")
|
|
178
|
+
dsa_el = at_child(elem, "DSAKeyValue")
|
|
179
|
+
ec_el = elem.children.find do |c|
|
|
180
|
+
c.is_a?(::Moxml::Element) &&
|
|
181
|
+
c.name == "ECKeyValue" &&
|
|
182
|
+
c.namespace_uri == DSIG11_NS
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
Model::KeyValue.new(
|
|
186
|
+
rsa_key_value: parse_rsa_key_value(rsa_el),
|
|
187
|
+
dsa_key_value: parse_dsa_key_value(dsa_el),
|
|
188
|
+
ec_key_value: parse_ec_key_value(ec_el),
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_rsa_key_value(elem)
|
|
193
|
+
return nil unless elem
|
|
194
|
+
|
|
195
|
+
Model::Key::RSAKeyValue.new(
|
|
196
|
+
modulus: text_of(elem, "Modulus"),
|
|
197
|
+
exponent: text_of(elem, "Exponent"),
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parse_dsa_key_value(elem)
|
|
202
|
+
return nil unless elem
|
|
203
|
+
|
|
204
|
+
Model::Key::DSAKeyValue.new(
|
|
205
|
+
p: text_of(elem, "P"),
|
|
206
|
+
q: text_of(elem, "Q"),
|
|
207
|
+
g: text_of(elem, "G"),
|
|
208
|
+
y: text_of(elem, "Y"),
|
|
209
|
+
j: text_of(elem, "J"),
|
|
210
|
+
seed: text_of(elem, "Seed"),
|
|
211
|
+
pgen_counter: text_of(elem, "PgenCounter"),
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parse_ec_key_value(elem)
|
|
216
|
+
return nil unless elem
|
|
217
|
+
|
|
218
|
+
named_curve_el = elem.children.find do |c|
|
|
219
|
+
c.is_a?(::Moxml::Element) &&
|
|
220
|
+
c.name == "NamedCurve" &&
|
|
221
|
+
c.namespace_uri == DSIG11_NS
|
|
222
|
+
end
|
|
223
|
+
public_key_el = elem.children.find do |c|
|
|
224
|
+
c.is_a?(::Moxml::Element) &&
|
|
225
|
+
c.name == "PublicKey" &&
|
|
226
|
+
c.namespace_uri == DSIG11_NS
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
Model::Key::ECKeyValue.new(
|
|
230
|
+
named_curve_uri: named_curve_el&.[]("URI"),
|
|
231
|
+
public_key: strip_base64(public_key_el&.text),
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def text_of(parent, local_name)
|
|
236
|
+
elem = at_child(parent, local_name)
|
|
237
|
+
elem&.text
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def strip_base64(text)
|
|
241
|
+
return nil if text.nil?
|
|
242
|
+
|
|
243
|
+
text.to_s.gsub(/\s+/, "")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def raw_key_info_children(elem)
|
|
247
|
+
# Children we don't model explicitly are preserved for round-trip
|
|
248
|
+
# fidelity (PGPData, SPKIData, MgmtData, dsig11:KeyInfoReference,
|
|
249
|
+
# dsig11:DEREncodedKeyValue, xenc:*).
|
|
250
|
+
elem.children.grep(::Moxml::Element).to_a
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def at_child(parent, local_name)
|
|
254
|
+
parent.children.find do |c|
|
|
255
|
+
c.is_a?(::Moxml::Element) &&
|
|
256
|
+
c.name == local_name &&
|
|
257
|
+
c.namespace_uri == DSIG_NS
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def at_children(parent, local_name)
|
|
262
|
+
parent.children.select do |c|
|
|
263
|
+
c.is_a?(::Moxml::Element) &&
|
|
264
|
+
c.name == local_name &&
|
|
265
|
+
c.namespace_uri == DSIG_NS
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -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
|