hanko_xml 0.1.0-aarch64-linux → 0.2.0-aarch64-linux

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.
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module HankoXml
6
+ module Xades
7
+ # The revocation evidence one certificate of the chain passed with (#168),
8
+ # on LongTermValidation#revocation.
9
+ # +source+ is :ocsp or :crl; +issued_at+ its thisUpdate; +status+ :good, or
10
+ # :revoked_later when it was revoked after the validation time.
11
+ RevocationStatus = Struct.new(:certificate, :source, :issued_at, :status, keyword_init: true) do
12
+ def initialize(**)
13
+ super
14
+ freeze
15
+ end
16
+ end
17
+
18
+ # A passed XAdES-LT validation (#168), on
19
+ # Xades::Verification#long_term_validation: the validation +time+ (the
20
+ # earliest verified SignatureTimeStamp), the signing +certificate_chain+
21
+ # (leaf first, ending at the caller's trust anchor), and the +revocation+
22
+ # evidence for each certificate below the anchor.
23
+ LongTermValidation = Struct.new(:time, :certificate_chain, :revocation, keyword_init: true) do
24
+ def initialize(**)
25
+ super
26
+ freeze
27
+ end
28
+ end
29
+
30
+ # The embedded <xades:CertificateValues> and <xades:RevocationValues> of
31
+ # one signature, parsed. Attacker-supplied until LongTermCheck uses them
32
+ # (S-22, S-23).
33
+ #
34
+ # @api private
35
+ class LongTermValues
36
+ attr_reader :certificates, :crls, :ocsp_responses
37
+
38
+ PATHS = {
39
+ certificates: "xades:CertificateValues/xades:EncapsulatedX509Certificate",
40
+ crls: "xades:RevocationValues/xades:CRLValues/xades:EncapsulatedCRLValue",
41
+ ocsp_responses: "xades:RevocationValues/xades:OCSPValues/xades:EncapsulatedOCSPValue"
42
+ }.freeze
43
+
44
+ # The values in +properties+ (an UnsignedSignatureProperties), or nil
45
+ # when it holds no CertificateValues or RevocationValues.
46
+ def self.read(properties, ns)
47
+ return nil unless properties
48
+ return nil if properties.xpath("xades:CertificateValues | xades:RevocationValues", ns).empty?
49
+
50
+ Locator.fail!("XAdES-LT values are verified only in the XAdES 1.3.2 namespace") unless ns["xades"] == NS
51
+ new(**PATHS.to_h { |kind, path| [kind, properties.xpath(path, ns).map { |node| parse(kind, node.text) }] })
52
+ end
53
+
54
+ def self.parse(kind, base64)
55
+ der = base64.gsub(/\s/, "").unpack1("m0")
56
+ case kind
57
+ when :certificates then OpenSSL::X509::Certificate.new(der)
58
+ when :crls then OpenSSL::X509::CRL.new(der)
59
+ else
60
+ OpenSSL::OCSP::Response.new(der).tap do |response|
61
+ unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL && response.basic
62
+ Locator.fail!("an <xades:EncapsulatedOCSPValue> is not a successful OCSP response")
63
+ end
64
+ end
65
+ end
66
+ rescue ArgumentError, OpenSSL::X509::CertificateError, OpenSSL::X509::CRLError, OpenSSL::OCSP::OCSPError
67
+ Locator.fail!("an embedded XAdES-LT value is not Base64 DER of its type (#{kind})")
68
+ end
69
+ private_class_method :parse
70
+
71
+ def initialize(certificates:, crls:, ocsp_responses:)
72
+ @certificates = certificates.freeze
73
+ @crls = crls.freeze
74
+ @ocsp_responses = ocsp_responses.freeze
75
+ freeze
76
+ end
77
+ end
78
+
79
+ # Validates the signing certificate chain at the time of the earliest
80
+ # verified SignatureTimeStamp, with the embedded revocation data (#168).
81
+ #
82
+ # The chain itself has already been built and verified at that time by
83
+ # {HankoXml::Verifier} (store:, StoreContext with time=). Here, each
84
+ # certificate below the trust anchor needs revocation evidence that:
85
+ #
86
+ # - verifies: an OCSP response through OpenSSL::OCSP::BasicResponse#verify
87
+ # (the responder's signature, its chain to the same anchor, and its
88
+ # authorization for that certificate), with the responder's chain then
89
+ # checked at the response's producedAt; or a CRL signed by the
90
+ # certificate's issuer (CRL#verify), an issuer allowed to sign CRLs;
91
+ # - is fresh: issued (thisUpdate) no earlier than the validation time
92
+ # minus +grace+, and not in the future;
93
+ # - does not show the certificate revoked at or before the validation
94
+ # time. Revoked later is accepted: the timestamp proves the signature
95
+ # existed before.
96
+ #
97
+ # Evidence that does not verify or is stale is not used; a certificate
98
+ # left without usable evidence fails, and so does any usable evidence of
99
+ # an earlier revocation (fail closed, S-23).
100
+ #
101
+ # @api private
102
+ class LongTermCheck
103
+ CRL_SIGN_BIT = 6
104
+
105
+ def initialize(grace:, clock_skew:)
106
+ @grace = grace
107
+ @clock_skew = clock_skew
108
+ end
109
+
110
+ # @return [LongTermValidation]
111
+ # @raise [XadesError]
112
+ def verify(time:, chain:, values:)
113
+ @time = time
114
+ @anchor = chain.last
115
+ @pool = (chain + values.certificates).uniq(&:to_der)
116
+ @values = values
117
+ revocation = chain.each_cons(2).with_index.map { |(certificate, issuer), i| status(certificate, issuer, i) }
118
+ LongTermValidation.new(time: time, certificate_chain: chain.freeze, revocation: revocation.freeze)
119
+ end
120
+
121
+ private
122
+
123
+ # +position+ names the certificate in messages: its subject comes from
124
+ # the document, so it goes to #xmlsec_errors, not the message (S-13).
125
+ def status(certificate, issuer, position)
126
+ evidence = ocsp_evidence(certificate, issuer) + crl_evidence(certificate, issuer)
127
+ revoked = evidence.find { |item| item.status == :revoked }
128
+ if revoked
129
+ fail!("the embedded #{revoked.source.upcase} shows #{name(position)} revoked at or before the " \
130
+ "validation time", certificate)
131
+ end
132
+ found = evidence.min_by { |item| item.status == :good ? 0 : 1 }
133
+ fail!("no embedded revocation data verifies and is fresh for #{name(position)}", certificate) unless found
134
+
135
+ found
136
+ end
137
+
138
+ def name(position)
139
+ position.zero? ? "the signing certificate" : "chain certificate #{position}"
140
+ end
141
+
142
+ def ocsp_evidence(certificate, issuer)
143
+ @values.ocsp_responses.flat_map do |response|
144
+ basic = response.basic
145
+ singles = basic.responses.select { |single| about?(single.certid, certificate, issuer) }
146
+ next [] if singles.empty? || !responder_verified?(basic)
147
+
148
+ singles.filter_map { |single| ocsp_status(certificate, single) }
149
+ end
150
+ end
151
+
152
+ def about?(certid, certificate, issuer)
153
+ return false unless certid.serial == certificate.serial
154
+
155
+ OpenSSL::OCSP::CertificateId.new(certificate, issuer, OpenSSL::Digest.new(certid.hash_algorithm)).cmp(certid)
156
+ rescue OpenSSL::OCSP::OCSPError, RuntimeError
157
+ false
158
+ end
159
+
160
+ def ocsp_status(certificate, single)
161
+ return nil unless fresh?(single.this_update)
162
+
163
+ status = case single.cert_status
164
+ when OpenSSL::OCSP::V_CERTSTATUS_GOOD then :good
165
+ when OpenSSL::OCSP::V_CERTSTATUS_REVOKED then revoked_status(single.revocation_time)
166
+ end
167
+ status && RevocationStatus.new(certificate: certificate, source: :ocsp, issued_at: single.this_update,
168
+ status: status)
169
+ end
170
+
171
+ def revoked_status(revoked_at)
172
+ revoked_at > @time ? :revoked_later : :revoked
173
+ end
174
+
175
+ # OpenSSL checks the signature, the responder's chain to the anchor and
176
+ # its authorization; it cannot take a verification time, so it runs
177
+ # without time checks and the responder's chain is then verified at
178
+ # producedAt.
179
+ def responder_verified?(basic)
180
+ response_certificates, responder_id, produced_at = ocsp_fields(basic)
181
+ pool = (@pool + response_certificates).uniq(&:to_der)
182
+ return false unless basic.verify(pool, anchor_store(OpenSSL::X509::V_FLAG_NO_CHECK_TIME))
183
+
184
+ signer = pool.find { |candidate| responder?(candidate, responder_id) }
185
+ return false unless signer
186
+
187
+ context = OpenSSL::X509::StoreContext.new(anchor_store, signer, pool)
188
+ context.time = produced_at
189
+ context.verify
190
+ end
191
+
192
+ # BasicOCSPResponse (RFC 6960 §4.2.1): its certs, ResponderID and
193
+ # producedAt, which OpenSSL::OCSP::BasicResponse does not expose.
194
+ def ocsp_fields(basic)
195
+ sequence = OpenSSL::ASN1.decode(basic.to_der).value
196
+ fields = sequence[0].value
197
+ fields = fields.drop(1) if fields.first.tag_class == :CONTEXT_SPECIFIC && fields.first.tag.zero?
198
+ certificates = sequence[3]&.value&.first&.value.to_a.map { |cert| OpenSSL::X509::Certificate.new(cert.to_der) }
199
+ [certificates, fields[0], fields[1].value]
200
+ end
201
+
202
+ # ResponderID: byName [1] or byKey [2] (SHA-1 of the public key bits).
203
+ def responder?(certificate, responder_id)
204
+ value = responder_id.value.first
205
+ if responder_id.tag == 1
206
+ certificate.subject.to_der == value.to_der
207
+ else
208
+ key_bits = OpenSSL::ASN1.decode(certificate.public_key.to_der).value[1].value
209
+ OpenSSL::Digest.digest("SHA1", key_bits) == value.value
210
+ end
211
+ end
212
+
213
+ def crl_evidence(certificate, issuer)
214
+ return [] unless crl_signer?(issuer)
215
+
216
+ @values.crls.filter_map do |crl|
217
+ next unless crl.issuer.to_der == issuer.subject.to_der && crl.verify(issuer.public_key)
218
+ next unless fresh?(crl.last_update)
219
+
220
+ entry = crl.revoked.find { |revoked| revoked.serial == certificate.serial }
221
+ status = entry ? revoked_status(entry.time) : :good
222
+ RevocationStatus.new(certificate: certificate, source: :crl, issued_at: crl.last_update, status: status)
223
+ end
224
+ end
225
+
226
+ # No KeyUsage means no restriction (RFC 5280); otherwise cRLSign.
227
+ def crl_signer?(issuer)
228
+ extension = issuer.extensions.find { |ext| ext.oid == "keyUsage" }
229
+ return true if extension.nil?
230
+
231
+ OpenSSL::ASN1.decode(extension.value_der).value.to_s.unpack1("B*").to_s[CRL_SIGN_BIT] == "1"
232
+ rescue OpenSSL::ASN1::ASN1Error
233
+ false
234
+ end
235
+
236
+ def fresh?(issued_at)
237
+ issued_at >= @time - @grace && issued_at <= Time.now + @clock_skew
238
+ end
239
+
240
+ def anchor_store(flags = nil)
241
+ OpenSSL::X509::Store.new.tap do |store|
242
+ store.add_cert(@anchor)
243
+ store.flags = flags if flags
244
+ end
245
+ end
246
+
247
+ def fail!(message, certificate)
248
+ raise HankoXml::XadesError.new(message, xmlsec_errors: ["subject: #{certificate.subject}"])
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module HankoXml
6
+ module Xades
7
+ # Obtains the RFC 3161 token for a XAdES-T <xades:SignatureTimeStamp>
8
+ # (#162). The imprint is a digest of the signature's <ds:SignatureValue>,
9
+ # canonicalized by xmlsec (#161) with the CanonicalizationMethod the
10
+ # SignatureTimeStamp skeleton already declares. OpenSSL::Timestamp builds
11
+ # the request and parses the response; the Signer's timestamper: performs
12
+ # the exchange.
13
+ #
14
+ # The response is checked against the request before it is used: status
15
+ # granted, the same imprint and digest, the same nonce. Its TSA signature
16
+ # is not checked here: that needs the TSA's trust anchors, which the
17
+ # verifier holds (#163).
18
+ #
19
+ # @api private
20
+ class SignatureTimeStamper
21
+ # The skeleton's CanonicalizationMethod and EncapsulatedTimeStamp, from
22
+ # the <ds:Signature>.
23
+ TIMESTAMP_PATH = [
24
+ [HankoXml::NS["ds"], "Object", nil], [NS, "QualifyingProperties", nil], [NS, "UnsignedProperties", nil],
25
+ [NS, "UnsignedSignatureProperties", nil], [NS, "SignatureTimeStamp", nil]
26
+ ].freeze
27
+ METHOD_PATH = [*TIMESTAMP_PATH, [HankoXml::NS["ds"], "CanonicalizationMethod", nil]].freeze
28
+ ENCAPSULATED_XPATH = "./ds:Object/xades:QualifyingProperties/xades:UnsignedProperties/" \
29
+ "xades:UnsignedSignatureProperties/xades:SignatureTimeStamp/xades:EncapsulatedTimeStamp"
30
+
31
+ # PKIStatus granted and grantedWithMods (RFC 3161 §2.4.2).
32
+ GRANTED = [0, 1].freeze
33
+
34
+ def initialize(signer)
35
+ @signer = signer
36
+ @digest = HankoXml::Algorithms.name_for(
37
+ :digest, HankoXml::Algorithms.uri_for(:digest, signer.timestamp_digest_method)
38
+ ).to_s.tr("_", "-")
39
+ end
40
+
41
+ def self.check_timestamper!(timestamper)
42
+ return if timestamper.nil? || timestamper.respond_to?(:call)
43
+
44
+ raise HankoXml::ConfigurationError, "timestamper: must respond to #call (TimeStampReq DER -> TimeStampResp DER)"
45
+ end
46
+
47
+ # The imprint algorithms go through the registry and +policy+, like
48
+ # every algorithm the signature itself uses.
49
+ def self.check_algorithms!(policy, digest:, canonicalization:)
50
+ { digest: digest, canonicalization: canonicalization }.each do |slot, name|
51
+ unless HankoXml::Algorithms.uri_for(slot, name)
52
+ raise HankoXml::ConfigurationError,
53
+ "timestamp_#{slot}_method: #{name.inspect} is not a registered #{slot} algorithm"
54
+ end
55
+
56
+ policy.enforce_algorithm!(slot, name)
57
+ end
58
+ end
59
+
60
+ # The DER TimeStampToken for the nth signature in +signed_bytes+.
61
+ # +method_path+ leads from the <ds:Signature> to the skeleton's
62
+ # CanonicalizationMethod.
63
+ def token(signed_bytes, index, method_path = METHOD_PATH)
64
+ token_for(Native.canonicalize(signed_bytes, index, [[HankoXml::NS["ds"], "SignatureValue", nil]],
65
+ method_path))
66
+ end
67
+
68
+ # The DER TimeStampToken over the digest of +input+ (an ArchiveTimeStamp's
69
+ # input, #169).
70
+ def token_for(input)
71
+ request = build_request(OpenSSL::Digest.digest(@digest, input))
72
+ response = exchange(request)
73
+ check_response!(request, response)
74
+ response.token.to_der
75
+ end
76
+
77
+ private
78
+
79
+ def build_request(imprint)
80
+ OpenSSL::Timestamp::Request.new.tap do |request|
81
+ request.algorithm = @digest
82
+ request.message_imprint = imprint
83
+ request.nonce = OpenSSL::BN.rand(64)
84
+ request.cert_requested = true
85
+ end
86
+ end
87
+
88
+ def exchange(request)
89
+ der = @signer.timestamper.call(request.to_der)
90
+ unless der.is_a?(String)
91
+ raise HankoXml::SigningError, "timestamper: returned #{der.class}, not a DER TimeStampResp"
92
+ end
93
+
94
+ OpenSSL::Timestamp::Response.new(der)
95
+ rescue OpenSSL::Timestamp::TimestampError
96
+ raise HankoXml::SigningError, "timestamper: returned bytes that are not a DER TimeStampResp"
97
+ end
98
+
99
+ def check_response!(request, response)
100
+ unless GRANTED.include?(response.status.to_i) && response.token
101
+ raise HankoXml::SigningError,
102
+ "the TSA did not grant the timestamp (status #{response.status.to_i}#{status_text(response)})"
103
+ end
104
+
105
+ info = response.token_info
106
+ unless info.message_imprint == request.message_imprint && digest_name(info.algorithm) == @digest
107
+ raise HankoXml::SigningError, "the TSA's token does not cover this signature's SignatureValue imprint"
108
+ end
109
+ return if info.nonce == request.nonce
110
+
111
+ raise HankoXml::SigningError, "the TSA's token does not carry the request's nonce"
112
+ end
113
+
114
+ def digest_name(algorithm)
115
+ OpenSSL::Digest.new(algorithm).name.downcase
116
+ rescue RuntimeError, OpenSSL::Digest::DigestError
117
+ nil
118
+ end
119
+
120
+ def status_text(response)
121
+ text = Array(response.status_text).join("; ")
122
+ text.empty? ? "" : ": #{text}"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ module HankoXml
129
+ module Xades
130
+ # A verified <xades:SignatureTimeStamp> (#163), on
131
+ # Xades::Verification#signature_timestamps, or <xades141:ArchiveTimeStamp>
132
+ # (#169), on Xades::Verification#archive_timestamps.
133
+ SignatureTimeStamp = Struct.new(:id, :time, :serial_number, :tsa_certificate, :digest_method, :policy_id,
134
+ keyword_init: true) do
135
+ def initialize(**)
136
+ super
137
+ freeze
138
+ end
139
+ end
140
+
141
+ # Verifies one <xades:SignatureTimeStamp> of a signature whose XML-DSig
142
+ # and signed properties already verified (#163).
143
+ #
144
+ # OpenSSL does the RFC 3161 work: OpenSSL::Timestamp::Response#verify
145
+ # checks the TSA's signature, its chain to the caller's TSA trust and the
146
+ # timeStamping purpose, and that the token's imprint is the digest of the
147
+ # bytes this class hands it. Those bytes are the <ds:SignatureValue>,
148
+ # canonicalized by xmlsec (#161) with the CanonicalizationMethod the
149
+ # SignatureTimeStamp declares. Both algorithms go through the policy.
150
+ #
151
+ # The element is attacker-controlled until this passes: it lies outside
152
+ # everything the signature covers.
153
+ #
154
+ # @api private
155
+ class SignatureTimeStampCheck
156
+ SIGNATURE_VALUE_PATH = [[HankoXml::NS["ds"], "SignatureValue", nil]].freeze
157
+ # The children a SignatureTimeStamp may have. Include and ReferenceInfo
158
+ # belong to other timestamp kinds, XMLTimeStamp has no defined format.
159
+ ALLOWED_CHILDREN = [[HankoXml::NS["ds"], "CanonicalizationMethod"], [:xades, "EncapsulatedTimeStamp"]].freeze
160
+
161
+ # +path+ is the native path from the <ds:Signature> to this
162
+ # SignatureTimeStamp.
163
+ def initialize(node, path, ns:, policy:, tsa_store:)
164
+ @node = node
165
+ @path = path
166
+ @ns = ns
167
+ @policy = policy
168
+ @tsa_store = tsa_store
169
+ end
170
+
171
+ # @return [SignatureTimeStamp]
172
+ # @raise [XadesError, PolicyError]
173
+ def verify(xml_bytes, signature_index)
174
+ check_children!
175
+ check_canonicalization_method!
176
+ canonical = imprint_input(xml_bytes, signature_index)
177
+ response = wrap(token_der)
178
+ digest = digest_method(response.token_info.algorithm)
179
+ verify_token!(response, digest, canonical)
180
+ result(response, digest)
181
+ rescue HankoXml::NativeError, HankoXml::ParseError
182
+ fail!("the #{covered} could not be canonicalized for the #{kind}")
183
+ end
184
+
185
+ private
186
+
187
+ def kind
188
+ "SignatureTimeStamp"
189
+ end
190
+
191
+ def element
192
+ "xades:SignatureTimeStamp"
193
+ end
194
+
195
+ def covered
196
+ "SignatureValue"
197
+ end
198
+
199
+ def method_path
200
+ [*@path, [HankoXml::NS["ds"], "CanonicalizationMethod", nil]]
201
+ end
202
+
203
+ # The bytes the token's imprint must be the digest of.
204
+ def imprint_input(xml_bytes, signature_index)
205
+ Native.canonicalize(xml_bytes, signature_index, SIGNATURE_VALUE_PATH, method_path)
206
+ end
207
+
208
+ def check_children!
209
+ children = @node.element_children
210
+ unless children.all? { |child| allowed_child?(child) }
211
+ fail!("a <#{element}> holds only CanonicalizationMethod and EncapsulatedTimeStamp")
212
+ end
213
+ return if children.count { |child| child.name == "EncapsulatedTimeStamp" } == 1
214
+
215
+ fail!("a <#{element}> needs exactly one <xades:EncapsulatedTimeStamp>")
216
+ end
217
+
218
+ def allowed_child?(child)
219
+ ALLOWED_CHILDREN.any? do |ns, name|
220
+ child.name == name && child.namespace&.href == (ns == :xades ? @ns["xades"] : ns)
221
+ end
222
+ end
223
+
224
+ def check_canonicalization_method!
225
+ methods = @node.xpath("ds:CanonicalizationMethod", @ns)
226
+ unless methods.size == 1 && methods.first["Algorithm"]
227
+ fail!("a <#{element}> without a CanonicalizationMethod is not supported")
228
+ end
229
+
230
+ @policy.enforce_algorithm!(:canonicalization, methods.first["Algorithm"])
231
+ end
232
+
233
+ def token_der
234
+ @node.at_xpath("xades:EncapsulatedTimeStamp", @ns).text.gsub(/\s/, "").unpack1("m0")
235
+ rescue ArgumentError
236
+ fail!("<xades:EncapsulatedTimeStamp> is not Base64")
237
+ end
238
+
239
+ # A TimeStampToken is a ContentInfo; OpenSSL verifies it as the token of
240
+ # a granted TimeStampResp, so it is wrapped in one.
241
+ def wrap(token)
242
+ status = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(0)])
243
+ OpenSSL::Timestamp::Response.new(OpenSSL::ASN1::Sequence([status, OpenSSL::ASN1.decode(token)]).to_der)
244
+ rescue OpenSSL::ASN1::ASN1Error, OpenSSL::Timestamp::TimestampError
245
+ fail!("<xades:EncapsulatedTimeStamp> is not an RFC 3161 TimeStampToken OpenSSL can read")
246
+ end
247
+
248
+ # The token's imprint digest, as a registry URI the policy allows.
249
+ def digest_method(openssl_name)
250
+ name = OpenSSL::Digest.new(openssl_name.to_s).name.downcase.tr("-", "_").to_sym
251
+ uri = HankoXml::Algorithms.uri_for(:digest, name)
252
+ fail!("the #{kind} imprint digest is not a registered digest") unless uri
253
+
254
+ @policy.enforce_algorithm!(:digest, uri)
255
+ [name, uri]
256
+ rescue RuntimeError, OpenSSL::Digest::DigestError
257
+ fail!("the #{kind} imprint digest is not a registered digest")
258
+ end
259
+
260
+ def verify_token!(response, digest, canonical)
261
+ request = OpenSSL::Timestamp::Request.new
262
+ request.algorithm = digest.first.to_s.tr("_", "-")
263
+ request.message_imprint = OpenSSL::Digest.digest(request.algorithm, canonical)
264
+ response.verify(request, @tsa_store)
265
+ rescue OpenSSL::Timestamp::TimestampError
266
+ fail!("the #{kind} does not verify: its TSA is not trusted, its signature is invalid, " \
267
+ "or it does not cover this signature's #{covered}")
268
+ end
269
+
270
+ def result(response, digest)
271
+ info = response.token_info
272
+ SignatureTimeStamp.new(
273
+ id: @node["Id"]&.dup&.freeze,
274
+ time: info.gen_time.utc.freeze,
275
+ serial_number: info.serial_number.to_i,
276
+ tsa_certificate: response.tsa_certificate,
277
+ digest_method: digest.last,
278
+ policy_id: info.policy_id
279
+ )
280
+ end
281
+
282
+ def fail!(message)
283
+ raise HankoXml::XadesError, message
284
+ end
285
+ end
286
+
287
+ # Verifies one <xades141:ArchiveTimeStamp> (XAdES-LTA, #169) as
288
+ # SignatureTimeStampCheck does a SignatureTimeStamp, over the input EN 319
289
+ # 132-1 §5.5.2.2 defines, concatenated in this order:
290
+ #
291
+ # 1. each SignedInfo Reference's data, as xmlsec digested it
292
+ # (Native.reference_data);
293
+ # 2. ds:SignedInfo, ds:SignatureValue and ds:KeyInfo (if present);
294
+ # 3. every UnsignedSignatureProperties child before this ArchiveTimeStamp;
295
+ # 4. every ds:Object except the one holding QualifyingProperties;
296
+ #
297
+ # 2 to 4 each canonicalized by xmlsec (#161) with the ArchiveTimeStamp's
298
+ # CanonicalizationMethod. A Reference whose data is a node set was
299
+ # canonicalized by xmlsec with inclusive c14n, so it is only accepted when
300
+ # that is the ArchiveTimeStamp's method too.
301
+ #
302
+ # @api private
303
+ class ArchiveTimeStampCheck < SignatureTimeStampCheck
304
+ INCLUSIVE_C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
305
+ BASE64_TRANSFORM = "http://www.w3.org/2000/09/xmldsig#base64"
306
+
307
+ # Whether any SignedInfo Reference leaves a node set: its transforms do
308
+ # not end in a canonicalization or base64, so xmlsec canonicalized it
309
+ # with inclusive c14n before digesting.
310
+ def self.node_set_references?(signature_info)
311
+ signature_info.references.any? do |reference|
312
+ last = reference.transforms.last
313
+ last.nil? || !(HankoXml::Algorithms.name_for(:canonicalization, last) || last == BASE64_TRANSFORM)
314
+ end
315
+ end
316
+
317
+ # The native paths of EN 319 132-1 §5.5.2.2 steps 2 to 4 for the
318
+ # ArchiveTimeStamp +node+ in +properties+ (an UnsignedSignatureProperties
319
+ # at native +path+).
320
+ def self.element_paths(signature, qualifying, properties, node, path)
321
+ ds = HankoXml::NS["ds"]
322
+ paths = [[[ds, "SignedInfo", nil]], [[ds, "SignatureValue", nil]]]
323
+ paths << [[ds, "KeyInfo", nil]] if signature.at_xpath("ds:KeyInfo", "ds" => ds)
324
+ properties.element_children.take_while { |child| !child.equal?(node) }.each do |child|
325
+ paths << [*path, sibling_step(child)]
326
+ end
327
+ signature.xpath("ds:Object", "ds" => ds).each_with_index do |object, i|
328
+ paths << [[ds, "Object", i]] unless object.equal?(qualifying.parent)
329
+ end
330
+ paths
331
+ end
332
+
333
+ def self.sibling_step(child)
334
+ href = child.namespace&.href
335
+ Locator.fail!("an unsigned signature property has no namespace") unless href
336
+
337
+ same = child.parent.element_children.select do |other|
338
+ other.name == child.name && other.namespace&.href == href
339
+ end
340
+ [href, child.name, same.index(child)]
341
+ end
342
+ private_class_method :sibling_step
343
+
344
+ # The concatenated input: the References' data, then each element at
345
+ # +element_paths+ canonicalized with the method at +method_path+.
346
+ def self.input(xml_bytes, signature_index, references, element_paths, method_path)
347
+ element_paths.inject(references.map(&:b).join.b) do |bytes, path|
348
+ bytes << Native.canonicalize(xml_bytes, signature_index, path, method_path).b
349
+ end
350
+ end
351
+
352
+ # +references+: the octets of step 1; +node_set_references+: whether any
353
+ # of them was a node set; +element_paths+: the native paths of 2 to 4.
354
+ def initialize(node, path, references:, node_set_references:, element_paths:, **options)
355
+ super(node, path, **options)
356
+ @references = references
357
+ @node_set_references = node_set_references
358
+ @element_paths = element_paths
359
+ end
360
+
361
+ private
362
+
363
+ def kind
364
+ "ArchiveTimeStamp"
365
+ end
366
+
367
+ def element
368
+ "xades141:ArchiveTimeStamp"
369
+ end
370
+
371
+ def covered
372
+ "signed data, signature and validation data"
373
+ end
374
+
375
+ def imprint_input(xml_bytes, signature_index)
376
+ method = @node.at_xpath("ds:CanonicalizationMethod", @ns)["Algorithm"]
377
+ if @node_set_references && method != INCLUSIVE_C14N
378
+ fail!("an ArchiveTimeStamp over a Reference whose data is a node set needs inclusive c14n " \
379
+ "as its CanonicalizationMethod")
380
+ end
381
+
382
+ self.class.input(xml_bytes, signature_index, @references, @element_paths, method_path)
383
+ end
384
+ end
385
+ end
386
+ end