hanko_xml 0.1.0 → 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/CHANGELOG.md +66 -0
- data/Cargo.lock +1 -1
- data/README.md +2 -1
- data/ext/hanko_xml/Cargo.toml +1 -1
- data/ext/hanko_xml/src/dsig.rs +220 -0
- data/ext/hanko_xml/src/lib.rs +90 -1
- data/lib/hanko_xml/signer.rb +4 -1
- data/lib/hanko_xml/verifier.rb +10 -3
- data/lib/hanko_xml/version.rb +1 -1
- data/lib/hanko_xml/xades/augmenter.rb +248 -0
- data/lib/hanko_xml/xades/locator.rb +67 -0
- data/lib/hanko_xml/xades/long_term.rb +252 -0
- data/lib/hanko_xml/xades/timestamp.rb +386 -0
- data/lib/hanko_xml/xades/validation_data.rb +115 -0
- data/lib/hanko_xml/xades/verifier.rb +311 -37
- data/lib/hanko_xml/xades.rb +93 -6
- data/sig/hanko_xml/native.rbs +2 -0
- data/sig/hanko_xml/xades.rbs +91 -1
- metadata +6 -1
|
@@ -11,11 +11,30 @@ module HankoXml
|
|
|
11
11
|
class Verification < HankoXml::Verification
|
|
12
12
|
# @return [SignedProperties, nil] Only when #valid?.
|
|
13
13
|
attr_reader :signed_properties
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# @return [Array<SignatureTimeStamp>, nil] The verified SignatureTimeStamps (#163), in document order;
|
|
15
|
+
# empty when there are none, nil unless #valid? or with timestamps: :ignore.
|
|
16
|
+
attr_reader :signature_timestamps
|
|
17
|
+
# @return [LongTermValidation, nil] The XAdES-LT validation (#168): only when #valid? and the signature
|
|
18
|
+
# carries CertificateValues or RevocationValues that were checked.
|
|
19
|
+
attr_reader :long_term_validation
|
|
20
|
+
# @return [Array<SignatureTimeStamp>, nil] The verified xades141:ArchiveTimeStamps (#169), in document
|
|
21
|
+
# order; empty when there are none, nil unless #valid? or with timestamps: :ignore.
|
|
22
|
+
attr_reader :archive_timestamps
|
|
23
|
+
|
|
24
|
+
def initialize(signed_properties: nil, signature_timestamps: nil, long_term_validation: nil,
|
|
25
|
+
archive_timestamps: nil, **attributes)
|
|
16
26
|
@signed_properties = signed_properties
|
|
27
|
+
@signature_timestamps = signature_timestamps&.freeze
|
|
28
|
+
@long_term_validation = long_term_validation
|
|
29
|
+
@archive_timestamps = archive_timestamps&.freeze
|
|
17
30
|
super(**attributes)
|
|
18
31
|
end
|
|
32
|
+
|
|
33
|
+
# The earliest verified SignatureTimeStamp time: when a trusted third
|
|
34
|
+
# party saw this signature at the latest. Nil without one.
|
|
35
|
+
def timestamp_time
|
|
36
|
+
signature_timestamps&.map(&:time)&.min
|
|
37
|
+
end
|
|
19
38
|
end
|
|
20
39
|
|
|
21
40
|
# Verifies a XAdES-B signature (#58): the XML-DSig signature through
|
|
@@ -31,14 +50,44 @@ module HankoXml
|
|
|
31
50
|
# Reads XAdES 1.3.2 and the older 1.2.2 and 1.1.1 namespaces (#86); the
|
|
32
51
|
# whole signature must use one.
|
|
33
52
|
#
|
|
53
|
+
# XAdES-T (#163): every <xades:SignatureTimeStamp> in the signature's
|
|
54
|
+
# UnsignedSignatureProperties must verify against the TSA trust
|
|
55
|
+
# (tsa_store: / tsa_certificates:), kept apart from the signer's. A
|
|
56
|
+
# timestamp that does not verify fails the signature (fail closed).
|
|
57
|
+
# Xades::Locator finds QualifyingProperties and its unsigned containers.
|
|
58
|
+
#
|
|
59
|
+
# XAdES-LT (#168): when the signature embeds CertificateValues or
|
|
60
|
+
# RevocationValues, the signing certificate is validated at the time of
|
|
61
|
+
# the earliest verified SignatureTimeStamp: XML-DSig trust (chain,
|
|
62
|
+
# validity dates) at that time, then revocation evidence for every
|
|
63
|
+
# certificate below the trust anchor from the embedded CRLs and OCSP
|
|
64
|
+
# responses (Xades::LongTermCheck). A certificate that has expired since
|
|
65
|
+
# still verifies; embedded data that does not hold fails (fail closed).
|
|
66
|
+
# XAdES-LTA (#169): every xades141:ArchiveTimeStamp must verify too, over
|
|
67
|
+
# the EN 319 132-1 input (Xades::ArchiveTimeStampCheck), against the same
|
|
68
|
+
# TSA trust; fail closed, and not read with timestamps: :ignore.
|
|
69
|
+
#
|
|
34
70
|
# Needs certificate-based trust (certificate:, certificates:, store:), and
|
|
35
71
|
# Nokogiri: the signed properties are read from the DOM (#102).
|
|
36
|
-
# Unsigned properties (XAdES-T/LT/LTA) are ignored: #81, #82.
|
|
37
72
|
class Verifier < HankoXml::Verifier # rubocop:disable Metrics/ClassLength
|
|
73
|
+
TIMESTAMP_MODES = %i[verify require ignore].freeze
|
|
74
|
+
LONG_TERM_MODES = %i[verify require ignore].freeze
|
|
75
|
+
|
|
38
76
|
# @param signature_policy [SignaturePolicy, Hash, :implied, nil] When given, the signed
|
|
39
77
|
# SignaturePolicyIdentifier must be exactly this.
|
|
78
|
+
# @param timestamps [Symbol] :verify (default) checks every SignatureTimeStamp present; :require also
|
|
79
|
+
# fails a signature without one; :ignore does not read them, the XAdES-B behavior.
|
|
80
|
+
# @param tsa_store [OpenSSL::X509::Store, nil] Trust for the TSAs, not the signer.
|
|
81
|
+
# @param tsa_certificates [Array<OpenSSL::X509::Certificate, String, Key>, nil] Trusted TSA roots or
|
|
82
|
+
# certificates, instead of tsa_store:.
|
|
83
|
+
# @param long_term [Symbol, nil] :verify checks embedded XAdES-LT values when present; :require also
|
|
84
|
+
# fails a signature without them; :ignore does not read them. nil (default) is :verify, or :ignore with
|
|
85
|
+
# timestamps: :ignore, since the validation time comes from a SignatureTimeStamp.
|
|
86
|
+
# @param revocation_grace [Numeric] Seconds an embedded CRL or OCSP response may predate the validation
|
|
87
|
+
# time and still count as fresh. 0 (default): it must be issued at or after it.
|
|
40
88
|
# All other keywords are {HankoXml::Verifier}'s.
|
|
41
|
-
def initialize(signature_policy: nil,
|
|
89
|
+
def initialize(signature_policy: nil, timestamps: :verify, tsa_store: nil, tsa_certificates: nil,
|
|
90
|
+
long_term: nil, revocation_grace: 0, **options)
|
|
42
91
|
Dom.require!("HankoXml::Xades::Verifier")
|
|
43
92
|
if options[:key] || options[:keys]
|
|
44
93
|
raise HankoXml::ConfigurationError,
|
|
@@ -47,6 +96,12 @@ module HankoXml
|
|
|
47
96
|
end
|
|
48
97
|
|
|
49
98
|
@expected_signature_policy = coerce_signature_policy(signature_policy)
|
|
99
|
+
@timestamps = coerce_timestamps(timestamps)
|
|
100
|
+
@tsa_store = coerce_tsa_store(tsa_store, tsa_certificates)
|
|
101
|
+
@long_term = coerce_long_term(long_term)
|
|
102
|
+
@revocation_grace = coerce_revocation_grace(revocation_grace)
|
|
103
|
+
# Kept to verify the XML-DSig signature again at the validation time.
|
|
104
|
+
@options = options.freeze
|
|
50
105
|
super(**options)
|
|
51
106
|
end
|
|
52
107
|
|
|
@@ -59,25 +114,109 @@ module HankoXml
|
|
|
59
114
|
end
|
|
60
115
|
|
|
61
116
|
def verify(xml)
|
|
62
|
-
|
|
117
|
+
as_xades(super)
|
|
63
118
|
end
|
|
64
119
|
|
|
65
120
|
def verify_all(xml)
|
|
66
|
-
super.map { |verification|
|
|
121
|
+
super.map { |verification| as_xades(verification) }
|
|
67
122
|
end
|
|
68
123
|
|
|
69
124
|
private
|
|
70
125
|
|
|
71
|
-
|
|
126
|
+
# Every signature verified goes through here, with the bytes and the
|
|
127
|
+
# index the native layer used, which the timestamp checks reuse.
|
|
128
|
+
#
|
|
129
|
+
# With XAdES-LT values, the SignatureTimeStamps are verified first, as
|
|
130
|
+
# their earliest time is when the signing certificate is validated
|
|
131
|
+
# (#168): the XML-DSig signature is then verified at that time.
|
|
132
|
+
def verify_single(input, index)
|
|
133
|
+
values, timestamps = long_term_plan(input, index)
|
|
134
|
+
return xades(super, input.bytes, index) unless values
|
|
135
|
+
|
|
136
|
+
time = timestamps.map(&:time).min
|
|
137
|
+
verification = xades(at_time(time, values, input, index), input.bytes, index, timestamps: timestamps)
|
|
138
|
+
return verification unless verification.valid?
|
|
139
|
+
|
|
140
|
+
validation = LongTermCheck.new(grace: @revocation_grace, clock_skew: @policy.clock_skew)
|
|
141
|
+
.verify(time: time, chain: long_term_chain(verification), values: values)
|
|
142
|
+
rebuild(verification, signed_properties: verification.signed_properties,
|
|
143
|
+
signature_timestamps: verification.signature_timestamps,
|
|
144
|
+
archive_timestamps: verification.archive_timestamps,
|
|
145
|
+
long_term_validation: validation)
|
|
146
|
+
rescue HankoXml::XadesError, HankoXml::PolicyError => e
|
|
147
|
+
rebuild(verification || super, error: e)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The embedded XAdES-LT values and the verified SignatureTimeStamps, or
|
|
151
|
+
# nil when there are no values to check.
|
|
152
|
+
def long_term_plan(input, index)
|
|
153
|
+
return nil if @long_term == :ignore
|
|
154
|
+
|
|
155
|
+
signature = signature_nodes(input)[index]
|
|
156
|
+
values = long_term_values(signature)
|
|
157
|
+
unless values
|
|
158
|
+
if @long_term == :require
|
|
159
|
+
fail!("long_term: :require, but the signature has no <xades:CertificateValues> or " \
|
|
160
|
+
"<xades:RevocationValues>")
|
|
161
|
+
end
|
|
162
|
+
return nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
timestamps = signature_timestamps(signature, input.bytes, index)
|
|
166
|
+
if timestamps.empty?
|
|
167
|
+
fail!("XAdES-LT values need a verified <xades:SignatureTimeStamp> for their validation time")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
[values, timestamps]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A signature whose QualifyingProperties or unsigned containers cannot
|
|
174
|
+
# be located has no values here: the signed-properties and timestamp
|
|
175
|
+
# checks fail it on the same structure, with their own message.
|
|
176
|
+
def long_term_values(signature)
|
|
177
|
+
qualifying, ns = Locator.qualifying_properties(signature)
|
|
178
|
+
properties = Locator.unsigned_signature_properties(qualifying, ns)
|
|
179
|
+
rescue HankoXml::XadesError
|
|
180
|
+
nil
|
|
181
|
+
else
|
|
182
|
+
LongTermValues.read(properties, ns)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The XML-DSig verification at +time+ (unless the caller fixed
|
|
186
|
+
# verification_time:), with the embedded CertificateValues as untrusted
|
|
187
|
+
# intermediates for a store: chain.
|
|
188
|
+
def at_time(time, values, input, index)
|
|
189
|
+
verifier = AtTime.new(intermediates: values.certificates, signature_policy: @expected_signature_policy,
|
|
190
|
+
timestamps: :ignore, long_term: :ignore,
|
|
191
|
+
**@options.merge(verification_time: verification_time || time))
|
|
192
|
+
HankoXml::Verifier.instance_method(:verify_single).bind_call(verifier, input, index)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# store: gives the chain OpenSSL validated; certificate: and
|
|
196
|
+
# certificates: pin the signing certificate itself as the anchor.
|
|
197
|
+
def long_term_chain(verification)
|
|
198
|
+
verification.certificate_chain || [verification.certificate]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A result that never reached verify_single (no signature found).
|
|
202
|
+
def as_xades(verification)
|
|
203
|
+
verification.is_a?(Verification) ? verification : rebuild(verification)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def xades(verification, xml_bytes, index, timestamps: nil)
|
|
72
207
|
return rebuild(verification) unless verification.valid?
|
|
73
208
|
|
|
74
209
|
properties = signed_properties(verification)
|
|
75
|
-
|
|
210
|
+
timestamps ||= signature_timestamps(verification.signature_node, xml_bytes, index)
|
|
211
|
+
archives = archive_timestamps(verification, xml_bytes, index)
|
|
212
|
+
rebuild(verification, signed_properties: properties, signature_timestamps: timestamps,
|
|
213
|
+
archive_timestamps: archives)
|
|
76
214
|
rescue HankoXml::XadesError, HankoXml::PolicyError => e
|
|
77
215
|
rebuild(verification, error: e)
|
|
78
216
|
end
|
|
79
217
|
|
|
80
|
-
def rebuild(verification, signed_properties: nil,
|
|
218
|
+
def rebuild(verification, signed_properties: nil, signature_timestamps: nil, long_term_validation: nil,
|
|
219
|
+
archive_timestamps: nil, error: nil)
|
|
81
220
|
Verification.new(
|
|
82
221
|
valid: verification.valid? && error.nil?,
|
|
83
222
|
errors: [*verification.errors, *error],
|
|
@@ -94,10 +233,153 @@ module HankoXml
|
|
|
94
233
|
reference_element_indexes: verification.reference_element_indexes,
|
|
95
234
|
reference_valid: verification.reference_valid,
|
|
96
235
|
signature_element_index: verification.signature_element_index,
|
|
97
|
-
signed_properties: signed_properties
|
|
236
|
+
signed_properties: signed_properties,
|
|
237
|
+
signature_timestamps: signature_timestamps,
|
|
238
|
+
long_term_validation: long_term_validation,
|
|
239
|
+
archive_timestamps: archive_timestamps
|
|
98
240
|
)
|
|
99
241
|
end
|
|
100
242
|
|
|
243
|
+
# Each SignatureTimeStamp of this signature, verified (#163). Read only
|
|
244
|
+
# along the path from its own QualifyingProperties, never by a search
|
|
245
|
+
# that a wrapped element elsewhere could satisfy.
|
|
246
|
+
def signature_timestamps(signature, xml_bytes, index)
|
|
247
|
+
return nil if @timestamps == :ignore
|
|
248
|
+
|
|
249
|
+
nodes, path, xades_ns = signature_timestamp_nodes(signature)
|
|
250
|
+
if nodes.empty?
|
|
251
|
+
fail!("timestamps: :require, but the signature has no <xades:SignatureTimeStamp>") if @timestamps == :require
|
|
252
|
+
return []
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
unless @tsa_store
|
|
256
|
+
fail!("the signature has a <xades:SignatureTimeStamp>; pass tsa_store: or tsa_certificates: to " \
|
|
257
|
+
"verify it, or timestamps: :ignore")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
nodes.each_with_index.map do |node, i|
|
|
261
|
+
SignatureTimeStampCheck.new(node, [*path, [xades_ns, "SignatureTimeStamp", i]],
|
|
262
|
+
ns: { "ds" => HankoXml::NS["ds"], "xades" => xades_ns },
|
|
263
|
+
policy: @policy, tsa_store: @tsa_store).verify(xml_bytes, index)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Each xades141:ArchiveTimeStamp of this signature, verified (#169), read
|
|
268
|
+
# along the same path as the SignatureTimeStamps.
|
|
269
|
+
def archive_timestamps(verification, xml_bytes, index)
|
|
270
|
+
return nil if @timestamps == :ignore
|
|
271
|
+
|
|
272
|
+
signature = verification.signature_node
|
|
273
|
+
qualifying, ns = Locator.qualifying_properties(signature)
|
|
274
|
+
properties = Locator.unsigned_signature_properties(qualifying, ns)
|
|
275
|
+
nodes = properties ? properties.xpath("xades141:ArchiveTimeStamp", "xades141" => NS141).to_a : []
|
|
276
|
+
return [] if nodes.empty?
|
|
277
|
+
|
|
278
|
+
fail!("ArchiveTimeStamp is verified only with XAdES 1.3.2 QualifyingProperties") unless ns["xades"] == NS
|
|
279
|
+
unless @tsa_store
|
|
280
|
+
fail!("the signature has a <xades141:ArchiveTimeStamp>; pass tsa_store: or tsa_certificates: to " \
|
|
281
|
+
"verify it, or timestamps: :ignore")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
path = Locator.unsigned_path(signature, qualifying, NS)
|
|
285
|
+
references = reference_data(verification, xml_bytes, index)
|
|
286
|
+
node_set = ArchiveTimeStampCheck.node_set_references?(verification.signature_info)
|
|
287
|
+
nodes.each_with_index.map do |node, i|
|
|
288
|
+
ArchiveTimeStampCheck.new(node, [*path, [NS141, "ArchiveTimeStamp", i]],
|
|
289
|
+
references: references, node_set_references: node_set,
|
|
290
|
+
element_paths: ArchiveTimeStampCheck.element_paths(signature, qualifying,
|
|
291
|
+
properties, node, path),
|
|
292
|
+
ns: { "ds" => HankoXml::NS["ds"], "xades" => NS },
|
|
293
|
+
policy: @policy, tsa_store: @tsa_store).verify(xml_bytes, index)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Each SignedInfo Reference's data as xmlsec digested it, verifying the
|
|
298
|
+
# signature again with the key that verified it.
|
|
299
|
+
def reference_data(verification, xml_bytes, index)
|
|
300
|
+
octets = Native.reference_data(xml_bytes, [verification.certificate.public_key.to_pem], native_id_attributes,
|
|
301
|
+
index, @referenced_documents.to_a, @policy.allows?(:transform, :xpath_filter2))
|
|
302
|
+
fail!("a Reference's data could not be read for the ArchiveTimeStamp") if octets.any?(&:nil?)
|
|
303
|
+
|
|
304
|
+
octets
|
|
305
|
+
rescue HankoXml::NativeError
|
|
306
|
+
fail!("the signature's Reference data could not be read for the ArchiveTimeStamp")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The SignatureTimeStamp elements, the native path to their parent, and
|
|
310
|
+
# their XAdES namespace.
|
|
311
|
+
def signature_timestamp_nodes(signature)
|
|
312
|
+
qualifying, ns = Locator.qualifying_properties(signature)
|
|
313
|
+
properties = Locator.unsigned_signature_properties(qualifying, ns)
|
|
314
|
+
return [[], nil] unless properties
|
|
315
|
+
|
|
316
|
+
timestamps = properties.xpath("xades:SignatureTimeStamp", ns)
|
|
317
|
+
if timestamps.any? && ns["xades"] != NS
|
|
318
|
+
fail!("SignatureTimeStamp is verified only in the XAdES 1.3.2 namespace")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
[timestamps.to_a, Locator.unsigned_path(signature, qualifying, ns["xades"]), ns["xades"]]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def coerce_long_term(value)
|
|
325
|
+
value = @timestamps == :ignore ? :ignore : :verify if value.nil?
|
|
326
|
+
unless LONG_TERM_MODES.include?(value)
|
|
327
|
+
raise HankoXml::ConfigurationError, "long_term: must be one of #{LONG_TERM_MODES.inspect}"
|
|
328
|
+
end
|
|
329
|
+
if value != :ignore && @timestamps == :ignore
|
|
330
|
+
raise HankoXml::ConfigurationError,
|
|
331
|
+
"long_term: #{value.inspect} needs SignatureTimeStamps for its validation time; " \
|
|
332
|
+
"it cannot go with timestamps: :ignore"
|
|
333
|
+
end
|
|
334
|
+
if value == :require && @tsa_store.nil?
|
|
335
|
+
raise HankoXml::ConfigurationError, "long_term: :require needs tsa_store: or tsa_certificates:"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
value
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def coerce_revocation_grace(value)
|
|
342
|
+
return value if value.is_a?(Numeric) && value >= 0
|
|
343
|
+
|
|
344
|
+
raise HankoXml::ConfigurationError, "revocation_grace: must be a number of seconds, 0 or more"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def coerce_timestamps(value)
|
|
348
|
+
return value if TIMESTAMP_MODES.include?(value)
|
|
349
|
+
|
|
350
|
+
raise HankoXml::ConfigurationError, "timestamps: must be one of #{TIMESTAMP_MODES.inspect}"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# The TSA trust, separate from the signer's: tsa_store: as given, or a
|
|
354
|
+
# store over tsa_certificates:.
|
|
355
|
+
def coerce_tsa_store(store, certificates)
|
|
356
|
+
raise HankoXml::ConfigurationError, "pass tsa_store: or tsa_certificates:, not both" if store && certificates
|
|
357
|
+
|
|
358
|
+
if @timestamps == :require && !store && !certificates
|
|
359
|
+
raise HankoXml::ConfigurationError, "timestamps: :require needs tsa_store: or tsa_certificates:"
|
|
360
|
+
end
|
|
361
|
+
return tsa_store_from(certificates) if store.nil?
|
|
362
|
+
return store if store.is_a?(OpenSSL::X509::Store)
|
|
363
|
+
|
|
364
|
+
raise HankoXml::ConfigurationError, "tsa_store: must be an OpenSSL::X509::Store"
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def tsa_store_from(certificates)
|
|
368
|
+
return nil if certificates.nil?
|
|
369
|
+
|
|
370
|
+
list = Array(certificates)
|
|
371
|
+
raise HankoXml::ConfigurationError, "tsa_certificates: must not be empty" if list.empty?
|
|
372
|
+
|
|
373
|
+
OpenSSL::X509::Store.new.tap do |store|
|
|
374
|
+
list.each do |certificate|
|
|
375
|
+
key = HankoXml::Key.coerce(certificate)
|
|
376
|
+
raise HankoXml::ConfigurationError, "tsa_certificates: must be X.509 certificates" unless key.certificate?
|
|
377
|
+
|
|
378
|
+
store.add_cert(key.to_openssl)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
101
383
|
def signed_properties(verification)
|
|
102
384
|
node, ns = signed_properties_node(verification)
|
|
103
385
|
signature_properties = node.at_xpath("xades:SignedSignatureProperties", ns)
|
|
@@ -122,7 +404,7 @@ module HankoXml
|
|
|
122
404
|
# with.
|
|
123
405
|
def signed_properties_node(verification)
|
|
124
406
|
info, reference = signed_properties_reference(verification)
|
|
125
|
-
qualifying, ns = qualifying_properties(verification.signature_node)
|
|
407
|
+
qualifying, ns = Locator.qualifying_properties(verification.signature_node)
|
|
126
408
|
|
|
127
409
|
type_namespace = VERSIONED_SIGNED_PROPERTIES_TYPES[info.type]
|
|
128
410
|
if type_namespace && type_namespace != ns["xades"]
|
|
@@ -153,22 +435,6 @@ module HankoXml
|
|
|
153
435
|
pairs.first
|
|
154
436
|
end
|
|
155
437
|
|
|
156
|
-
# This signature's single QualifyingProperties, in any XAdES namespace,
|
|
157
|
-
# and the namespaces to read it with.
|
|
158
|
-
def qualifying_properties(signature)
|
|
159
|
-
signature_id = signature["Id"].to_s
|
|
160
|
-
fail!("the <Signature> has no Id for QualifyingProperties to target") if signature_id.empty?
|
|
161
|
-
|
|
162
|
-
qualifying = signature.xpath("ds:Object/*[local-name()='QualifyingProperties']", "ds" => HankoXml::NS["ds"])
|
|
163
|
-
.select { |node| NAMESPACES.include?(node.namespace&.href) }
|
|
164
|
-
fail!("a XAdES signature has exactly one <xades:QualifyingProperties>") unless qualifying.size == 1
|
|
165
|
-
|
|
166
|
-
qualifying = qualifying.first
|
|
167
|
-
fail!("<xades:QualifyingProperties> does not target this <Signature>") unless targets?(qualifying, signature_id)
|
|
168
|
-
|
|
169
|
-
[qualifying, { "ds" => HankoXml::NS["ds"], "xades" => qualifying.namespace.href }]
|
|
170
|
-
end
|
|
171
|
-
|
|
172
438
|
# By its Type, or, with no Type (as some 1.2.2 signatures write it), by
|
|
173
439
|
# what xmlsec resolved and digested: a SignedProperties element in a
|
|
174
440
|
# XAdES namespace. Never by the URI text (#86).
|
|
@@ -179,16 +445,6 @@ module HankoXml
|
|
|
179
445
|
!node.nil? && node.name == "SignedProperties" && NAMESPACES.include?(node.namespace&.href)
|
|
180
446
|
end
|
|
181
447
|
|
|
182
|
-
# Target is "#" + the Signature Id. The older namespaces also had
|
|
183
|
-
# signers write the bare Id (Target="Signature"). Either way the
|
|
184
|
-
# QualifyingProperties must sit in this <Signature>'s own <Object>.
|
|
185
|
-
def targets?(qualifying, signature_id)
|
|
186
|
-
target = qualifying["Target"]
|
|
187
|
-
return true if target == "##{signature_id}"
|
|
188
|
-
|
|
189
|
-
LEGACY_NAMESPACES.include?(qualifying.namespace.href) && target == signature_id
|
|
190
|
-
end
|
|
191
|
-
|
|
192
448
|
def check_signing_certificate!(signature_properties, certificate, ns)
|
|
193
449
|
fail!("no certificate verified the signature, so SigningCertificate cannot be checked") unless certificate
|
|
194
450
|
|
|
@@ -314,6 +570,24 @@ module HankoXml
|
|
|
314
570
|
def fail!(message)
|
|
315
571
|
raise HankoXml::XadesError, message
|
|
316
572
|
end
|
|
573
|
+
|
|
574
|
+
# The XML-DSig verification of an XAdES-LT signature (#168): embedded
|
|
575
|
+
# certificates may complete the store: chain, never be its anchor or
|
|
576
|
+
# its signing certificate.
|
|
577
|
+
#
|
|
578
|
+
# @api private
|
|
579
|
+
class AtTime < Verifier
|
|
580
|
+
def initialize(intermediates:, **options)
|
|
581
|
+
@intermediates = intermediates
|
|
582
|
+
super(**options)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
private
|
|
586
|
+
|
|
587
|
+
def chain_intermediates
|
|
588
|
+
@intermediates
|
|
589
|
+
end
|
|
590
|
+
end
|
|
317
591
|
end
|
|
318
592
|
end
|
|
319
593
|
end
|
data/lib/hanko_xml/xades.rb
CHANGED
|
@@ -12,6 +12,10 @@ module HankoXml
|
|
|
12
12
|
module Xades
|
|
13
13
|
NS = "http://uri.etsi.org/01903/v1.3.2#"
|
|
14
14
|
|
|
15
|
+
# XAdES 1.4.1 (EN 319 132-1), the namespace of ArchiveTimeStamp and
|
|
16
|
+
# TimeStampValidationData (#169).
|
|
17
|
+
NS141 = "http://uri.etsi.org/01903/v1.4.1#"
|
|
18
|
+
|
|
15
19
|
# Earlier XAdES namespaces (TS 101 903 v1.1.1, v1.2.2) that Xades::Verifier
|
|
16
20
|
# also reads (#86). Xades::Signer writes only NS.
|
|
17
21
|
LEGACY_NAMESPACES = %w[http://uri.etsi.org/01903/v1.2.2# http://uri.etsi.org/01903/v1.1.1#].freeze
|
|
@@ -70,8 +74,8 @@ module HankoXml
|
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
# The authenticated view of a verified <xades:SignedProperties>
|
|
73
|
-
# (Xades::Verification#signed_properties).
|
|
74
|
-
# (
|
|
77
|
+
# (Xades::Verification#signed_properties). Timestamps are
|
|
78
|
+
# Xades::Verification#signature_timestamps (#163).
|
|
75
79
|
SignedProperties = Struct.new(:signing_time, :signature_policy, :claimed_roles, :data_object_formats,
|
|
76
80
|
keyword_init: true) do
|
|
77
81
|
def initialize(**)
|
|
@@ -88,10 +92,17 @@ module HankoXml
|
|
|
88
92
|
# certificate:), and optionally SignaturePolicyIdentifier, SignerRoleV2
|
|
89
93
|
# and DataObjectFormat.
|
|
90
94
|
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
95
|
+
# With timestamper:, a XAdES-T signature (#162): an RFC 3161 token over
|
|
96
|
+
# the canonicalized <ds:SignatureValue> in a <xades:SignatureTimeStamp>.
|
|
97
|
+
#
|
|
98
|
+
# With validation_data: as well, XAdES-LT (#167): CertificateValues and
|
|
99
|
+
# RevocationValues after the SignatureTimeStamp. Xades::Augmenter adds
|
|
100
|
+
# either to a signature that already exists.
|
|
101
|
+
#
|
|
102
|
+
# Always builds a new <Signature> (template: :build).
|
|
93
103
|
class Signer < HankoXml::Signer
|
|
94
|
-
attr_reader :signing_time, :signature_policy, :claimed_roles, :data_object_format
|
|
104
|
+
attr_reader :signing_time, :signature_policy, :claimed_roles, :data_object_format,
|
|
105
|
+
:timestamper, :timestamp_canonicalization_method, :validation_data
|
|
95
106
|
|
|
96
107
|
# @param certificate [Key, OpenSSL::X509::Certificate, String, Pathname] Required: SigningCertificateV2
|
|
97
108
|
# commits to it.
|
|
@@ -102,9 +113,18 @@ module HankoXml
|
|
|
102
113
|
# of this Signer's own references, which get an Id when they have none.
|
|
103
114
|
# @param signature_id [String, nil] The <Signature> Id; generated when nil, since QualifyingProperties
|
|
104
115
|
# targets it.
|
|
116
|
+
# @param timestamper [#call, nil] Called with a DER RFC 3161 TimeStampReq, returns the TSA's DER
|
|
117
|
+
# TimeStampResp. The library never opens a connection: the callable owns the TSA exchange.
|
|
118
|
+
# @param timestamp_digest_method [Symbol, nil] The imprint digest; nil means digest_method:.
|
|
119
|
+
# @param timestamp_canonicalization_method [Symbol] How <ds:SignatureValue> is canonicalized for the
|
|
120
|
+
# imprint.
|
|
121
|
+
# @param validation_data [ValidationData, Hash, nil] Certificates, CRLs and OCSP responses to embed
|
|
122
|
+
# (XAdES-LT); needs timestamper:, since B-LT builds on B-T.
|
|
105
123
|
# All other keywords are {HankoXml::Signer}'s.
|
|
106
124
|
def initialize(certificate: nil, signing_time: nil, signature_policy: nil, claimed_roles: [],
|
|
107
|
-
data_object_format: nil, signature_id: nil, template: :build,
|
|
125
|
+
data_object_format: nil, signature_id: nil, template: :build, timestamper: nil,
|
|
126
|
+
timestamp_digest_method: nil, timestamp_canonicalization_method: :exc_c14n, validation_data: nil,
|
|
127
|
+
**options, &block)
|
|
108
128
|
unless template.to_sym == :build
|
|
109
129
|
raise HankoXml::ConfigurationError,
|
|
110
130
|
"template: #{template.inspect} is not supported for XAdES; it always builds a new <Signature>"
|
|
@@ -117,6 +137,14 @@ module HankoXml
|
|
|
117
137
|
@signature_policy = coerce_signature_policy(signature_policy)
|
|
118
138
|
@claimed_roles = Array(claimed_roles).map { |role| role.to_s.dup.freeze }.freeze
|
|
119
139
|
@data_object_format = coerce_data_object_format(data_object_format)
|
|
140
|
+
SignatureTimeStamper.check_timestamper!(timestamper)
|
|
141
|
+
@timestamper = timestamper
|
|
142
|
+
@timestamp_digest_method = timestamp_digest_method
|
|
143
|
+
@timestamp_canonicalization_method = timestamp_canonicalization_method
|
|
144
|
+
@validation_data = ValidationData.coerce(validation_data)
|
|
145
|
+
if @validation_data && !timestamper
|
|
146
|
+
raise HankoXml::ConfigurationError, "validation_data: needs timestamper: (XAdES-LT builds on XAdES-T)"
|
|
147
|
+
end
|
|
120
148
|
|
|
121
149
|
super(certificate: certificate, signature_id: signature_id || "xades-#{SecureRandom.hex(8)}",
|
|
122
150
|
template: :build, **options, &block)
|
|
@@ -142,8 +170,37 @@ module HankoXml
|
|
|
142
170
|
"#{signature_id}-signedprops"
|
|
143
171
|
end
|
|
144
172
|
|
|
173
|
+
# The imprint digest: timestamp_digest_method:, or else digest_method:.
|
|
174
|
+
def timestamp_digest_method
|
|
175
|
+
@timestamp_digest_method || digest_method
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def signature_timestamp_id
|
|
179
|
+
"#{signature_id}-sigts"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def to_h
|
|
183
|
+
super.merge(signing_time: signing_time, signature_policy: signature_policy, claimed_roles: claimed_roles,
|
|
184
|
+
data_object_format: data_object_format, timestamper: timestamper,
|
|
185
|
+
timestamp_digest_method: timestamp_digest_method,
|
|
186
|
+
timestamp_canonicalization_method: timestamp_canonicalization_method,
|
|
187
|
+
validation_data: validation_data)
|
|
188
|
+
end
|
|
189
|
+
|
|
145
190
|
private
|
|
146
191
|
|
|
192
|
+
# xmlsec has signed; before the signed values reach the caller's
|
|
193
|
+
# document, obtain the timestamp over the new SignatureValue. A failure
|
|
194
|
+
# raises before anything is written back.
|
|
195
|
+
def write_back_signature(doc, signed_bytes, index, caller_sig_node)
|
|
196
|
+
token = timestamper && SignatureTimeStamper.new(self).token(signed_bytes, index)
|
|
197
|
+
super
|
|
198
|
+
return unless token
|
|
199
|
+
|
|
200
|
+
caller_sig_node.at_xpath(SignatureTimeStamper::ENCAPSULATED_XPATH, "ds" => HankoXml::NS["ds"], "xades" => NS)
|
|
201
|
+
.content = [token].pack("m0")
|
|
202
|
+
end
|
|
203
|
+
|
|
147
204
|
def signed_properties_reference
|
|
148
205
|
HankoXml::Reference.new(uri: "##{signed_properties_id}", type: SIGNED_PROPERTIES_TYPE,
|
|
149
206
|
transforms: [:exc_c14n], digest_method: digest_method)
|
|
@@ -179,6 +236,10 @@ module HankoXml
|
|
|
179
236
|
# Checked with the key material, after the block may have set certificate:.
|
|
180
237
|
def check_key_material!
|
|
181
238
|
super
|
|
239
|
+
if timestamper
|
|
240
|
+
SignatureTimeStamper.check_algorithms!(policy, digest: timestamp_digest_method,
|
|
241
|
+
canonicalization: timestamp_canonicalization_method)
|
|
242
|
+
end
|
|
182
243
|
return if certificate.is_a?(HankoXml::Key) && certificate.certificate?
|
|
183
244
|
|
|
184
245
|
raise HankoXml::ConfigurationError, "XAdES needs certificate:, which SigningCertificateV2 commits to"
|
|
@@ -207,6 +268,7 @@ module HankoXml
|
|
|
207
268
|
|
|
208
269
|
build_signature_properties(xades(signed, "SignedSignatureProperties"))
|
|
209
270
|
build_data_object_properties(signed) if @signer.data_object_format
|
|
271
|
+
build_unsigned_properties(qualifying) if @signer.timestamper
|
|
210
272
|
object
|
|
211
273
|
end
|
|
212
274
|
|
|
@@ -247,6 +309,26 @@ module HankoXml
|
|
|
247
309
|
hash.add_child(element("DigestValue")).content = policy.digest_value
|
|
248
310
|
end
|
|
249
311
|
|
|
312
|
+
# SignatureTimeStamp, then any validation data (#167).
|
|
313
|
+
def build_unsigned_properties(qualifying)
|
|
314
|
+
unsigned = xades(xades(qualifying, "UnsignedProperties"), "UnsignedSignatureProperties")
|
|
315
|
+
build_signature_timestamp(unsigned)
|
|
316
|
+
@signer.validation_data&.append_to(unsigned, @xades_ns)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The SignatureTimeStamp skeleton: the CanonicalizationMethod is in the
|
|
320
|
+
# document before signing, so xmlsec reads it to canonicalize the
|
|
321
|
+
# SignatureValue (#161), as the verifier will. No Reference covers
|
|
322
|
+
# UnsignedProperties, so filling EncapsulatedTimeStamp afterwards
|
|
323
|
+
# changes nothing signed.
|
|
324
|
+
def build_signature_timestamp(unsigned)
|
|
325
|
+
timestamp = xades(unsigned, "SignatureTimeStamp")
|
|
326
|
+
timestamp["Id"] = @signer.signature_timestamp_id
|
|
327
|
+
method = timestamp.add_child(element("CanonicalizationMethod"))
|
|
328
|
+
method["Algorithm"] = HankoXml::Algorithms.uri_for(:canonicalization, @signer.timestamp_canonicalization_method)
|
|
329
|
+
xades(timestamp, "EncapsulatedTimeStamp")
|
|
330
|
+
end
|
|
331
|
+
|
|
250
332
|
def build_data_object_properties(signed)
|
|
251
333
|
properties = xades(signed, "SignedDataObjectProperties")
|
|
252
334
|
format = @signer.data_object_format
|
|
@@ -284,4 +366,9 @@ module HankoXml
|
|
|
284
366
|
end
|
|
285
367
|
end
|
|
286
368
|
|
|
369
|
+
require_relative "xades/locator"
|
|
370
|
+
require_relative "xades/timestamp"
|
|
371
|
+
require_relative "xades/validation_data"
|
|
372
|
+
require_relative "xades/augmenter"
|
|
373
|
+
require_relative "xades/long_term"
|
|
287
374
|
require_relative "xades/verifier"
|
data/sig/hanko_xml/native.rbs
CHANGED
|
@@ -8,5 +8,7 @@ module HankoXml
|
|
|
8
8
|
def self.sign: (String xml_bytes, String key_pem, Array[String] cert_pems, Array[String] id_attributes, Integer index) -> String
|
|
9
9
|
def self.scan: (String xml_bytes, Array[String] id_attributes, bool strict) -> Hash[Symbol, untyped]
|
|
10
10
|
def self.validate_schema: (String xml_bytes, Integer index, String xsd_path) -> Array[String]
|
|
11
|
+
def self.reference_data: (String xml_bytes, Array[String] trusted_pems, Array[String] id_attributes, Integer index, Array[[String, String]] referenced_documents, bool allow_xpath_filter2) -> Array[String?]
|
|
12
|
+
def self.canonicalize: (String xml_bytes, Integer index, Array[[String, String, Integer?]] target_path, Array[[String, String, Integer?]] method_path) -> String
|
|
11
13
|
end
|
|
12
14
|
end
|