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 +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +2 -1
- data/lib/hanko_xml/3.1/hanko_xml.so +0 -0
- data/lib/hanko_xml/3.2/hanko_xml.so +0 -0
- data/lib/hanko_xml/3.3/hanko_xml.so +0 -0
- data/lib/hanko_xml/3.4/hanko_xml.so +0 -0
- 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 +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5dbc235211e7031b7bf4d32375716b790a0cdd3d57866babc430c7f861edaff7
|
|
4
|
+
data.tar.gz: ef42208f4f0fe811c8e5b02ddcb3da813758be210de357f5e4ce6abe0c83fe5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
data/lib/hanko_xml/signer.rb
CHANGED
|
@@ -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
|
-
|
|
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"])
|
data/lib/hanko_xml/verifier.rb
CHANGED
|
@@ -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
|
data/lib/hanko_xml/version.rb
CHANGED
|
@@ -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
|