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.
@@ -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
- def initialize(signed_properties: nil, **attributes)
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, **options)
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
- xades(super)
117
+ as_xades(super)
63
118
  end
64
119
 
65
120
  def verify_all(xml)
66
- super.map { |verification| xades(verification) }
121
+ super.map { |verification| as_xades(verification) }
67
122
  end
68
123
 
69
124
  private
70
125
 
71
- def xades(verification)
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
- rebuild(verification, signed_properties: properties)
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, error: 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
@@ -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). Unsigned properties
74
- # (timestamps, validation data) are never read.
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
- # Always builds a new <Signature> (template: :build). Timestamps and
92
- # validation data (XAdES-T/LT/LTA) are separate: #81, #82.
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, **options, &block)
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"
@@ -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