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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0b6411c7bfd43b93201dc27a5be27486692c17b795591a79794e32ad044c6cc
4
- data.tar.gz: 5de806837fa525410a6218de1afad38878cc5a1f2314174de1200ed745dbe0fa
3
+ metadata.gz: 5dbc235211e7031b7bf4d32375716b790a0cdd3d57866babc430c7f861edaff7
4
+ data.tar.gz: ef42208f4f0fe811c8e5b02ddcb3da813758be210de357f5e4ce6abe0c83fe5a
5
5
  SHA512:
6
- metadata.gz: 19f8fc0233cb84d1742f855fb7f4a0b92a21ebc2eb5f8add186b6ab5e1d035abf3b896682f09fda890be684017c5564c64b9a3e7f35121441582248a54427d68
7
- data.tar.gz: 8da69665c5dc95bb05a68cd320c1c40289b0b3d9784ecde1ec8193a24b28404fa449cdddd037f37c8badd4eccfcc419ff842f5a4b382150fc87291faf3b8c930
6
+ metadata.gz: 7be04c955748de9852f05046f65ae21e566f17fd6672ec33c62c58d3d95b92226987d7f48afacab96da9586adf80523ee19f0adc3e03f526292ff5b95335c635
7
+ data.tar.gz: fbe9e82aaa66c5a0c5aa00a4b138618135c622a7ca1eb11f181d12045c49c1523bfbb234107ec6a3fe23acbedd3626ae33e592cd5ba6517516b8fae12f22c112
data/CHANGELOG.md CHANGED
@@ -8,6 +8,72 @@ tracked separately, and is reported by `HankoXml::NativeVersion.build_versions`.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.0] - 2026-10-19
12
+
13
+ ### Added
14
+
15
+ - **#162:** XAdES-T signing. `Xades::Signer.new(timestamper: ->(request_der) { response_der })`
16
+ adds a `<xades:SignatureTimeStamp>`: an RFC 3161 token over the
17
+ `<ds:SignatureValue>`, canonicalized by xmlsec (#161). The callable does the
18
+ TSA exchange, so the library never opens a connection. The response must
19
+ grant the timestamp and carry the request's imprint and nonce, otherwise
20
+ signing raises `SigningError`. `timestamp_digest_method:` (default: the
21
+ Signer's `digest_method:`) and `timestamp_canonicalization_method:` (default
22
+ `:exc_c14n`) go through the Signer's policy.
23
+ - **#163:** XAdES-T verification. `Xades::Verifier` checks every
24
+ `<xades:SignatureTimeStamp>` of the signature against the TSA trust in
25
+ `tsa_store:` or `tsa_certificates:`, which is kept separate from the signer's.
26
+ OpenSSL verifies the token, and its imprint must cover the `<ds:SignatureValue>`
27
+ as xmlsec canonicalizes it. `Xades::Verification#signature_timestamps` lists
28
+ them (time, serial, TSA certificate) and `#timestamp_time` gives the earliest.
29
+ `timestamps: :require` also fails a signature without one (S-22).
30
+ - **#167:** XAdES-LT validation data. `Xades::Augmenter` adds unsigned
31
+ properties to an existing, signed XAdES signature: a `SignatureTimeStamp`
32
+ (`timestamper:`, B to T) and `CertificateValues` / `RevocationValues`
33
+ (`validation_data:`, T to LT), leaving everything signed byte-identical.
34
+ `Xades::Signer` takes `validation_data:` too, with `timestamper:`.
35
+ `Xades::ValidationData` holds caller-supplied certificates, CRLs and OCSP
36
+ responses, type-checked only; the library never fetches them, and nothing
37
+ embedded is trusted yet (#168).
38
+ - **#168:** XAdES-LT verification. When a signature embeds `CertificateValues`
39
+ or `RevocationValues`, `Xades::Verifier` validates the signing certificate
40
+ at the time of its earliest verified SignatureTimeStamp: XML-DSig trust at
41
+ that time (an expired certificate still verifies; embedded certificates may
42
+ complete a `store:` chain), then embedded OCSP responses or CRLs for every
43
+ certificate below the anchor, verified by OpenSSL against the same anchor
44
+ and issued no earlier than that time (`revocation_grace:` widens it).
45
+ `long_term: :verify | :require | :ignore`; the result is
46
+ `Xades::Verification#long_term_validation`. Fail closed (S-23).
47
+ - **#169:** XAdES-LTA. `Xades::Augmenter.new(timestamper:, archive_timestamp: true)`
48
+ appends a `<xades141:ArchiveTimeStamp>` over the EN 319 132-1 input (the
49
+ References' data as xmlsec digested it, SignedInfo, SignatureValue, KeyInfo,
50
+ every unsigned property before it and the other `ds:Object`s); running it
51
+ again renews it. `Xades::Verifier` verifies every ArchiveTimeStamp against
52
+ the TSA trust, failing closed, and lists them in
53
+ `Xades::Verification#archive_timestamps`. `Native.reference_data` returns
54
+ each Reference's digested octets from xmlsec. SignXML's DSS-produced
55
+ HU_POL-3 ArchiveTimeStamp verifies.
56
+
57
+ ### Changed
58
+
59
+ - **#163:** `Xades::Verifier` no longer ignores unsigned SignatureTimeStamps.
60
+ A signature that carries one now fails verification unless the timestamp
61
+ verifies, which needs `tsa_store:` or `tsa_certificates:`. Pass
62
+ `timestamps: :ignore` to keep the XAdES-B-only behavior.
63
+ - **#168:** `Xades::Verifier` no longer ignores embedded `CertificateValues` /
64
+ `RevocationValues`. A signature that carries them is validated at its
65
+ timestamp time and fails when that evidence does not hold, which can also
66
+ make a signature fail that verified before (and one whose certificate has
67
+ expired pass). Pass `long_term: :ignore` to keep the XAdES-T behavior.
68
+ - **#169:** `Xades::Verifier` no longer ignores `xades141:ArchiveTimeStamp`s:
69
+ one that does not verify, or one present without TSA trust, fails the
70
+ signature. `timestamps: :ignore` leaves them unread.
71
+
72
+ ### Fixed
73
+
74
+ - **#158:** Without Nokogiri, `Signer#sign!` and `Signer#template` given a String
75
+ now raise `NokogiriRequired` naming themselves instead of `HankoXml.parse`.
76
+
11
77
  ## [0.1.0] - 2026-10-15
12
78
 
13
79
  First release.
data/README.md CHANGED
@@ -18,9 +18,10 @@ It works nicely with Nokogiri, signing and verifying the documents you already h
18
18
  - **Full XML-DSig Support:** Sign and verify Enveloped, Enveloping, and Detached signatures.
19
19
  - **Nokogiri Native, Nokogiri Optional:** Signs and verifies the Nokogiri documents you already hold, with opt-in `doc.sign!` / `doc.signature_valid?` shorthands (`require "hanko_xml/nokogiri_ext"`), and verifies Strings, files and IOs without Nokogiri.
20
20
  - **XSW Defense:** Provides a robust `Policy` engine and structural `signed?(node)` checks to defeat XML Signature Wrapping (XSW) attacks.
21
+ - **XAdES, B to LTA:** `HankoXml::Xades::Signer`, `Augmenter` and `Verifier` sign, timestamp (RFC 3161, through a TSA exchange you supply), embed validation data and add renewable archive timestamps, and verify all of it fail closed. See the [XAdES guide](docs/src/content/guide/xades.md).
21
22
  - **Rust-Backed:** Memory-safe FFI boundaries, zero shell-outs to CLI binaries, and massive performance gains over pure-Ruby cryptography.
22
23
 
23
- A feature and security comparison with Python [SignXML](https://pypi.org/project/signxml/), including a plan to close the remaining gaps (XAdES first), lives in [`docs/src/content/guide/comparison-signxml.md`](docs/src/content/guide/comparison-signxml.md).
24
+ A feature and security comparison with Python [SignXML](https://pypi.org/project/signxml/), including the plan that closed the XAdES gap, lives in [`docs/src/content/guide/comparison-signxml.md`](docs/src/content/guide/comparison-signxml.md).
24
25
 
25
26
  ## Requirements
26
27
 
Binary file
Binary file
Binary file
Binary file
@@ -134,6 +134,7 @@ module HankoXml
134
134
  # it into +doc_or_xml+ and performs no cryptography -- for inspection
135
135
  # and testing (§5.2).
136
136
  def template(doc_or_xml)
137
+ Dom.require!("HankoXml::Signer#template")
137
138
  doc = HankoXml.coerce_document(doc_or_xml)
138
139
  HankoXml::Template.build(doc, self)
139
140
  end
@@ -185,8 +186,10 @@ module HankoXml
185
186
  node_signer = signer_for_node(doc_or_xml)
186
187
  return node_signer.sign!(doc_or_xml.document, force: force, signature: signature) if node_signer
187
188
 
188
- doc = HankoXml.coerce_document(doc_or_xml, policy: policy)
189
+ # Before coercing: a String would otherwise fail inside HankoXml.parse
190
+ # and name that instead of the method the caller used.
189
191
  Dom.require!("HankoXml::Signer#sign!")
192
+ doc = HankoXml.coerce_document(doc_or_xml, policy: policy)
190
193
 
191
194
  signatures, adopted = find_or_build_signatures(doc)
192
195
  all_sigs_in_order = doc.xpath(".//ds:Signature", ds: NS["ds"])
@@ -510,7 +510,7 @@ module HankoXml
510
510
  HankoXml::UntrustedCertificate.new("No X509Certificate found in KeyInfo for store verification")]
511
511
  end
512
512
 
513
- chain, error = verified_chain_from_store(embedded)
513
+ chain, error = verified_chain_from_store(embedded, chain_intermediates)
514
514
  leaf = chain&.first
515
515
  error ||= signing_certificate_violation(leaf)
516
516
  return [nil, nil, error] if error
@@ -549,9 +549,16 @@ module HankoXml
549
549
  sig_info.certificates.map { |pem| OpenSSL::X509::Certificate.new(pem) }
550
550
  end
551
551
 
552
+ # Untrusted certificates that may complete a store: chain besides those in
553
+ # <KeyInfo>; never candidates for the signing certificate. Xades::Verifier
554
+ # supplies a signature's embedded CertificateValues (#168).
555
+ def chain_intermediates
556
+ []
557
+ end
558
+
552
559
  # The chain OpenSSL built and verified for the one end-entity certificate
553
560
  # that chains to the store, leaf first, or an error.
554
- def verified_chain_from_store(certificates)
561
+ def verified_chain_from_store(certificates, intermediates = [])
555
562
  candidates = end_entity_certificates(certificates)
556
563
  candidates = certificates if candidates.empty?
557
564
 
@@ -559,7 +566,7 @@ module HankoXml
559
566
  last_error = "Certificate chain validation failed"
560
567
 
561
568
  candidates.each do |leaf|
562
- chain = certificates.reject { |cert| cert.equal?(leaf) }
569
+ chain = certificates.reject { |cert| cert.equal?(leaf) } + intermediates
563
570
  ctx = OpenSSL::X509::StoreContext.new(@store, leaf, chain)
564
571
  if @policy.check_certificate_validity
565
572
  ctx.time = @verification_time if @verification_time
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HankoXml
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HankoXml
4
+ module Xades
5
+ # Adds unsigned properties to a XAdES signature that already exists
6
+ # (#167): a <xades:SignatureTimeStamp> (B to T), the validation data of
7
+ # XAdES-LT (T to LT), and a <xades141:ArchiveTimeStamp> (LT to LTA,
8
+ # #169; again later to renew it). Revocation data is best collected after the
9
+ # signature timestamp exists, so LT is usually added later, by the
10
+ # holder of the signed document rather than the signer.
11
+ #
12
+ # augmenter = HankoXml::Xades::Augmenter.new(
13
+ # validation_data: { certificates: chain, ocsp_responses: [ocsp_der] }
14
+ # )
15
+ # lt = augmenter.augment(signed_xml)
16
+ #
17
+ # Nothing a Reference covers changes: the new elements go into
18
+ # UnsignedSignatureProperties, found along the same fixed path
19
+ # Xades::Verifier reads (Xades::Locator), and the document is written
20
+ # back with HankoXml.serialize, so the existing digests and
21
+ # SignatureValue still verify.
22
+ #
23
+ # The Augmenter does not verify the signature it extends, and embedding
24
+ # material vouches for nothing: verify first, and a verifier treats the
25
+ # embedded values as untrusted (S-22). It never fetches: the timestamper:
26
+ # callable and the caller's validation data are its only inputs.
27
+ #
28
+ # Writes XAdES 1.3.2 only, and needs Nokogiri.
29
+ class Augmenter
30
+ attr_reader :timestamper, :timestamp_digest_method, :timestamp_canonicalization_method, :validation_data,
31
+ :archive_timestamp, :archive_canonicalization_method, :policy
32
+
33
+ # @param timestamper [#call, nil] As for {Xades::Signer}: adds a SignatureTimeStamp.
34
+ # @param timestamp_digest_method [Symbol] The imprint digest.
35
+ # @param timestamp_canonicalization_method [Symbol] How <ds:SignatureValue> is canonicalized for the
36
+ # imprint.
37
+ # @param validation_data [ValidationData, Hash, nil] Adds CertificateValues and RevocationValues; the
38
+ # signature must have a SignatureTimeStamp, or timestamper: adds one first.
39
+ # @param archive_timestamp [Boolean] Adds an ArchiveTimeStamp, last, from timestamper:, over the signature
40
+ # and all its validation data. timestamper: then adds a SignatureTimeStamp only when the signature has
41
+ # none. The signature must carry LT values, or validation_data: adds them first.
42
+ # @param archive_canonicalization_method [Symbol] The ArchiveTimeStamp's CanonicalizationMethod. Inclusive
43
+ # c14n by default: a Reference that leaves a node set (as Xades::Signer's enveloped one does) is
44
+ # covered with xmlsec's inclusive c14n, so any other method is refused for such a signature.
45
+ # @param policy [Policy] Checks the imprint algorithms and limits the document size.
46
+ def initialize(timestamper: nil, timestamp_digest_method: :sha256, timestamp_canonicalization_method: :exc_c14n,
47
+ validation_data: nil, archive_timestamp: false, archive_canonicalization_method: :c14n,
48
+ policy: HankoXml::Policy.strict)
49
+ SignatureTimeStamper.check_timestamper!(timestamper)
50
+ @timestamper = timestamper
51
+ @timestamp_digest_method = timestamp_digest_method
52
+ @timestamp_canonicalization_method = timestamp_canonicalization_method
53
+ @validation_data = ValidationData.coerce(validation_data)
54
+ @policy = policy
55
+ @archive_timestamp = archive_timestamp ? true : false
56
+ @archive_canonicalization_method = archive_canonicalization_method
57
+ nothing = !timestamper && !@validation_data
58
+ raise HankoXml::ConfigurationError, "pass timestamper:, validation_data: or both" if nothing
59
+ if @archive_timestamp && !timestamper
60
+ raise HankoXml::ConfigurationError, "archive_timestamp: needs timestamper:, which obtains it"
61
+ end
62
+
63
+ if timestamper
64
+ SignatureTimeStamper.check_algorithms!(policy, digest: timestamp_digest_method,
65
+ canonicalization: timestamp_canonicalization_method)
66
+ end
67
+ check_archive_canonicalization_method! if @archive_timestamp
68
+ freeze
69
+ end
70
+
71
+ # A copy of the document with the nth signature augmented.
72
+ #
73
+ # @param source [String, Nokogiri::XML::Document, IO, Pathname] The signed document.
74
+ # @param signature [Integer, nil] The document-order index of the <ds:Signature>; nil when the
75
+ # document has exactly one.
76
+ # @return [SignedDocument]
77
+ # @raise [XadesError] when the signature's XAdES structure cannot take the properties.
78
+ # @raise [SigningError] when the TSA's answer does not hold (as for Xades::Signer).
79
+ def augment(source, signature: nil)
80
+ Dom.require!("HankoXml::Xades::Augmenter")
81
+ doc = HankoXml.coerce_document(source, policy: policy).dup
82
+ signatures = doc.xpath("//ds:Signature", "ds" => HankoXml::NS["ds"])
83
+ index = signature_index(signatures.size, signature)
84
+ node = signatures[index]
85
+ value = node.at_xpath("ds:SignatureValue", "ds" => HankoXml::NS["ds"])
86
+ if value.nil? || value.text.strip.empty?
87
+ raise HankoXml::ConfigurationError, "the <Signature> is not signed yet; augment a signed document"
88
+ end
89
+
90
+ augment_signature(doc, node, index)
91
+ SignedDocument.new(doc)
92
+ end
93
+
94
+ def inspect
95
+ "#<#{self.class} timestamper=#{!timestamper.nil?} validation_data=#{!validation_data.nil?}>"
96
+ end
97
+
98
+ private
99
+
100
+ def check_archive_canonicalization_method!
101
+ unless HankoXml::Algorithms.uri_for(:canonicalization, archive_canonicalization_method)
102
+ raise HankoXml::ConfigurationError, "archive_canonicalization_method: " \
103
+ "#{archive_canonicalization_method.inspect} is not a registered " \
104
+ "canonicalization algorithm"
105
+ end
106
+
107
+ policy.enforce_algorithm!(:canonicalization, archive_canonicalization_method)
108
+ end
109
+
110
+ def signature_index(count, signature)
111
+ raise HankoXml::ConfigurationError, "the document has no <ds:Signature> to augment" if count.zero?
112
+
113
+ if signature.nil?
114
+ return 0 if count == 1
115
+
116
+ raise HankoXml::ConfigurationError, "the document has #{count} signatures; pass signature: (an index)"
117
+ end
118
+ unless signature.is_a?(Integer) && signature.between?(-count, count - 1)
119
+ raise HankoXml::ConfigurationError, "signature: #{signature.inspect} not found"
120
+ end
121
+
122
+ signature % count
123
+ end
124
+
125
+ def augment_signature(doc, signature, index)
126
+ qualifying, ns = Locator.qualifying_properties(signature)
127
+ Locator.fail!("Xades::Augmenter writes only XAdES 1.3.2 QualifyingProperties") unless ns["xades"] == NS
128
+
129
+ properties = Locator.unsigned_signature_properties(qualifying, ns) || create_unsigned(qualifying)
130
+ timestamped = properties.at_xpath("xades:SignatureTimeStamp", ns)
131
+ if timestamper && !(archive_timestamp && timestamped)
132
+ add_signature_timestamp(doc, signature, index, qualifying, properties)
133
+ end
134
+ add_validation_data(properties, qualifying, ns) if validation_data
135
+ add_archive_timestamp(doc, signature, index, qualifying, properties) if archive_timestamp
136
+ end
137
+
138
+ def add_validation_data(properties, qualifying, ns)
139
+ unless properties.at_xpath("xades:SignatureTimeStamp", ns)
140
+ raise HankoXml::ConfigurationError,
141
+ "XAdES-LT builds on XAdES-T: the signature has no <xades:SignatureTimeStamp>; pass timestamper: too"
142
+ end
143
+
144
+ validation_data.append_to(properties, qualifying.namespace)
145
+ end
146
+
147
+ # The skeleton goes in first, so xmlsec reads its CanonicalizationMethod;
148
+ # its input (ArchiveTimeStampCheck.input) is everything before it. The
149
+ # References' data comes from xmlsec verifying the signature with a
150
+ # certificate of its own KeyInfo: that vouches for nothing, but a signature
151
+ # that does not hold is not archived.
152
+ def add_archive_timestamp(doc, signature, index, qualifying, properties)
153
+ if properties.xpath("xades:CertificateValues | xades:RevocationValues", "xades" => NS).empty?
154
+ raise HankoXml::ConfigurationError,
155
+ "XAdES-LTA builds on XAdES-LT: the signature has no CertificateValues or RevocationValues; " \
156
+ "pass validation_data: too"
157
+ end
158
+
159
+ position = properties.xpath("xades141:ArchiveTimeStamp", "xades141" => NS141).size
160
+ archive = properties.add_child(element(doc, "ArchiveTimeStamp"))
161
+ attach(archive, archive.add_namespace_definition("xades141", NS141))
162
+ archive["Id"] = unique_id(doc, "#{signature["Id"]}-ats")
163
+ method = attach(archive.add_child(element(doc, "CanonicalizationMethod")), signature.namespace)
164
+ method["Algorithm"] = HankoXml::Algorithms.uri_for(:canonicalization, archive_canonicalization_method)
165
+ encapsulated = attach(archive.add_child(element(doc, "EncapsulatedTimeStamp")), qualifying.namespace)
166
+
167
+ bytes = HankoXml.serialize(doc)
168
+ path = Locator.unsigned_path(signature, qualifying, NS)
169
+ method_path = [*path, [NS141, "ArchiveTimeStamp", position],
170
+ [HankoXml::NS["ds"], "CanonicalizationMethod", nil]]
171
+ check_node_set_references!(bytes, index, method["Algorithm"])
172
+ input = ArchiveTimeStampCheck.input(bytes, index, reference_data(bytes, index, signature),
173
+ ArchiveTimeStampCheck.element_paths(signature, qualifying, properties,
174
+ archive, path),
175
+ method_path)
176
+ encapsulated.content = [SignatureTimeStamper.new(self).token_for(input)].pack("m0")
177
+ end
178
+
179
+ def check_node_set_references!(bytes, index, method)
180
+ info = HankoXml::SignatureInfo.from_native(Native.inspect_sig(bytes, index))
181
+ unless ArchiveTimeStampCheck.node_set_references?(info) && method != ArchiveTimeStampCheck::INCLUSIVE_C14N
182
+ return
183
+ end
184
+
185
+ raise HankoXml::ConfigurationError,
186
+ "a Reference of this signature leaves a node set, which an ArchiveTimeStamp covers with inclusive " \
187
+ "c14n only: pass archive_canonicalization_method: :c14n"
188
+ end
189
+
190
+ def reference_data(bytes, index, signature)
191
+ certificates = signature.xpath("ds:KeyInfo/ds:X509Data/ds:X509Certificate", "ds" => HankoXml::NS["ds"])
192
+ .map { |node| OpenSSL::X509::Certificate.new(node.text.gsub(/\s/, "").unpack1("m")).to_pem }
193
+ Native.reference_data(bytes, certificates, %w[Id ID id], index, [], policy.allows?(:transform, :xpath_filter2))
194
+ rescue HankoXml::NativeError, OpenSSL::X509::CertificateError, ArgumentError
195
+ Locator.fail!("an ArchiveTimeStamp needs the signature to verify with a <ds:X509Certificate> of its KeyInfo")
196
+ end
197
+
198
+ # UnsignedProperties (last in QualifyingProperties) and its
199
+ # UnsignedSignatureProperties (first in UnsignedProperties).
200
+ def create_unsigned(qualifying)
201
+ doc = qualifying.document
202
+ namespace = qualifying.namespace
203
+ unsigned = qualifying.at_xpath("xades:UnsignedProperties", "xades" => NS) ||
204
+ attach(qualifying.add_child(element(doc, "UnsignedProperties")), namespace)
205
+ properties = element(doc, "UnsignedSignatureProperties")
206
+ first = unsigned.children.first
207
+ attach(first ? first.add_previous_sibling(properties) : unsigned.add_child(properties), namespace)
208
+ end
209
+
210
+ # The skeleton goes in first, so xmlsec reads its CanonicalizationMethod
211
+ # (#161) from the bytes, as for Xades::Signer; then the token is filled in.
212
+ def add_signature_timestamp(doc, signature, index, qualifying, properties)
213
+ position = properties.xpath("xades:SignatureTimeStamp", "xades" => NS).size
214
+ timestamp = attach(properties.add_child(element(doc, "SignatureTimeStamp")), qualifying.namespace)
215
+ timestamp["Id"] = unique_id(doc, "#{signature["Id"]}-sigts")
216
+ method = attach(timestamp.add_child(element(doc, "CanonicalizationMethod")), signature.namespace)
217
+ method["Algorithm"] = HankoXml::Algorithms.uri_for(:canonicalization, timestamp_canonicalization_method)
218
+ encapsulated = attach(timestamp.add_child(element(doc, "EncapsulatedTimeStamp")), qualifying.namespace)
219
+
220
+ path = [*Locator.unsigned_path(signature, qualifying, NS), [NS, "SignatureTimeStamp", position],
221
+ [HankoXml::NS["ds"], "CanonicalizationMethod", nil]]
222
+ token = SignatureTimeStamper.new(self).token(HankoXml.serialize(doc), index, path)
223
+ encapsulated.content = [token].pack("m0")
224
+ end
225
+
226
+ def unique_id(doc, base)
227
+ taken = ->(id) { doc.at_xpath("//*[@Id=$id]", nil, id: id) }
228
+ return base unless taken.call(base)
229
+
230
+ (2..).each do |n|
231
+ candidate = "#{base}-#{n}"
232
+ return candidate unless taken.call(candidate)
233
+ end
234
+ end
235
+
236
+ def element(doc, name)
237
+ doc.create_element(name)
238
+ end
239
+
240
+ # The namespace is set once the node is in the tree, as
241
+ # QualifyingProperties#xades does, so no redundant declaration appears.
242
+ def attach(node, namespace)
243
+ node.namespace = namespace
244
+ node
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HankoXml
4
+ module Xades
5
+ # Finds a signature's <xades:QualifyingProperties> and its unsigned
6
+ # containers along fixed paths from the <ds:Signature>, never by a search
7
+ # that a wrapped element elsewhere could satisfy (S-22). Xades::Verifier
8
+ # reads, and Xades::Augmenter writes, exactly the elements found here.
9
+ #
10
+ # @api private
11
+ module Locator
12
+ module_function
13
+
14
+ # This signature's single QualifyingProperties, in any XAdES namespace,
15
+ # and the namespaces to read it with.
16
+ def qualifying_properties(signature)
17
+ signature_id = signature["Id"].to_s
18
+ fail!("the <Signature> has no Id for QualifyingProperties to target") if signature_id.empty?
19
+
20
+ qualifying = signature.xpath("ds:Object/*[local-name()='QualifyingProperties']", "ds" => HankoXml::NS["ds"])
21
+ .select { |node| NAMESPACES.include?(node.namespace&.href) }
22
+ fail!("a XAdES signature has exactly one <xades:QualifyingProperties>") unless qualifying.size == 1
23
+
24
+ qualifying = qualifying.first
25
+ fail!("<xades:QualifyingProperties> does not target this <Signature>") unless targets?(qualifying, signature_id)
26
+
27
+ [qualifying, { "ds" => HankoXml::NS["ds"], "xades" => qualifying.namespace.href }]
28
+ end
29
+
30
+ # Target is "#" + the Signature Id. The older namespaces also had
31
+ # signers write the bare Id (Target="Signature"). Either way the
32
+ # QualifyingProperties must sit in this <Signature>'s own <Object>.
33
+ def targets?(qualifying, signature_id)
34
+ target = qualifying["Target"]
35
+ return true if target == "##{signature_id}"
36
+
37
+ LEGACY_NAMESPACES.include?(qualifying.namespace.href) && target == signature_id
38
+ end
39
+
40
+ # The UnsignedSignatureProperties, or nil when there is none. More than
41
+ # one UnsignedProperties or UnsignedSignatureProperties is refused: which
42
+ # one a reader would pick is ambiguous.
43
+ def unsigned_signature_properties(qualifying, ns)
44
+ unsigned = qualifying.xpath("xades:UnsignedProperties", ns)
45
+ fail!("a XAdES signature has at most one <xades:UnsignedProperties>") if unsigned.size > 1
46
+ return nil if unsigned.empty?
47
+
48
+ properties = unsigned.first.xpath("xades:UnsignedSignatureProperties", ns)
49
+ fail!("a XAdES signature has at most one <xades:UnsignedSignatureProperties>") if properties.size > 1
50
+
51
+ properties.first
52
+ end
53
+
54
+ # The native path (Native.canonicalize) from the <ds:Signature> to its
55
+ # UnsignedSignatureProperties.
56
+ def unsigned_path(signature, qualifying, xades_ns)
57
+ object_index = signature.xpath("ds:Object", "ds" => HankoXml::NS["ds"]).index(qualifying.parent)
58
+ [[HankoXml::NS["ds"], "Object", object_index], [xades_ns, "QualifyingProperties", nil],
59
+ [xades_ns, "UnsignedProperties", nil], [xades_ns, "UnsignedSignatureProperties", nil]]
60
+ end
61
+
62
+ def fail!(message)
63
+ raise HankoXml::XadesError, message
64
+ end
65
+ end
66
+ end
67
+ end