hanko_xml 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/Cargo.lock +1 -1
- data/README.md +2 -1
- data/ext/hanko_xml/Cargo.toml +1 -1
- data/ext/hanko_xml/src/dsig.rs +220 -0
- data/ext/hanko_xml/src/lib.rs +90 -1
- data/lib/hanko_xml/signer.rb +4 -1
- data/lib/hanko_xml/verifier.rb +10 -3
- data/lib/hanko_xml/version.rb +1 -1
- data/lib/hanko_xml/xades/augmenter.rb +248 -0
- data/lib/hanko_xml/xades/locator.rb +67 -0
- data/lib/hanko_xml/xades/long_term.rb +252 -0
- data/lib/hanko_xml/xades/timestamp.rb +386 -0
- data/lib/hanko_xml/xades/validation_data.rb +115 -0
- data/lib/hanko_xml/xades/verifier.rb +311 -37
- data/lib/hanko_xml/xades.rb +93 -6
- data/sig/hanko_xml/native.rbs +2 -0
- data/sig/hanko_xml/xades.rbs +91 -1
- metadata +6 -1
|
@@ -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
|
|
@@ -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
|