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,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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module HankoXml
|
|
6
|
+
module Xades
|
|
7
|
+
# The XAdES-LT validation material (#167): certificates for
|
|
8
|
+
# <xades:CertificateValues>, CRLs and OCSP responses for
|
|
9
|
+
# <xades:RevocationValues>. The caller collects it; the library never
|
|
10
|
+
# fetches. Nothing here is trusted: a verifier treats it as
|
|
11
|
+
# attacker-supplied until it chains to its own anchors (S-22, #168).
|
|
12
|
+
#
|
|
13
|
+
# Each entry is checked for its type only: X.509 certificates, X.509
|
|
14
|
+
# CRLs, and OCSPResponses whose responseStatus is successful.
|
|
15
|
+
ValidationData = Struct.new(:certificates, :crls, :ocsp_responses, keyword_init: true) do
|
|
16
|
+
# @param certificates [Array<OpenSSL::X509::Certificate, String, Key>] Anything Key.coerce reads.
|
|
17
|
+
# @param crls [Array<OpenSSL::X509::CRL, String>] PEM or DER.
|
|
18
|
+
# @param ocsp_responses [Array<OpenSSL::OCSP::Response, String>] DER OCSPResponse.
|
|
19
|
+
def initialize(certificates: [], crls: [], ocsp_responses: [])
|
|
20
|
+
super(
|
|
21
|
+
certificates: Array(certificates).map { |value| ValidationData.certificate(value) }.freeze,
|
|
22
|
+
crls: Array(crls).map { |value| ValidationData.crl(value) }.freeze,
|
|
23
|
+
ocsp_responses: Array(ocsp_responses).map { |value| ValidationData.ocsp_response(value) }.freeze
|
|
24
|
+
)
|
|
25
|
+
if to_h.values.all?(&:empty?)
|
|
26
|
+
raise HankoXml::ConfigurationError, "validation_data: needs certificates:, crls: or ocsp_responses:"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.coerce(value)
|
|
33
|
+
case value
|
|
34
|
+
when nil, ValidationData then value
|
|
35
|
+
when Hash then new(**value)
|
|
36
|
+
else
|
|
37
|
+
raise HankoXml::ConfigurationError,
|
|
38
|
+
"validation_data: must be a Xades::ValidationData, a Hash of its keywords or nil"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.certificate(value)
|
|
43
|
+
key = HankoXml::Key.coerce(value)
|
|
44
|
+
return key.to_openssl if key.certificate?
|
|
45
|
+
|
|
46
|
+
raise HankoXml::ConfigurationError, "validation_data certificates: must be X.509 certificates"
|
|
47
|
+
rescue HankoXml::KeyLoadError, ::ArgumentError
|
|
48
|
+
raise HankoXml::ConfigurationError, "validation_data certificates: must be X.509 certificates"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.crl(value)
|
|
52
|
+
return value if value.is_a?(OpenSSL::X509::CRL)
|
|
53
|
+
raise HankoXml::ConfigurationError, "validation_data crls: must be X.509 CRLs" unless value.is_a?(String)
|
|
54
|
+
|
|
55
|
+
OpenSSL::X509::CRL.new(value)
|
|
56
|
+
rescue OpenSSL::X509::CRLError
|
|
57
|
+
raise HankoXml::ConfigurationError, "validation_data crls: must be X.509 CRLs (PEM or DER)"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.ocsp_response(value)
|
|
61
|
+
response = value.is_a?(OpenSSL::OCSP::Response) ? value : parse_ocsp_response(value)
|
|
62
|
+
unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL && response.basic
|
|
63
|
+
raise HankoXml::ConfigurationError,
|
|
64
|
+
"validation_data ocsp_responses: must be successful OCSP responses " \
|
|
65
|
+
"(responseStatus #{response.status})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
response
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.parse_ocsp_response(value)
|
|
72
|
+
unless value.is_a?(String)
|
|
73
|
+
raise HankoXml::ConfigurationError, "validation_data ocsp_responses: must be DER OCSPResponses"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
OpenSSL::OCSP::Response.new(value)
|
|
77
|
+
rescue OpenSSL::OCSP::OCSPError
|
|
78
|
+
raise HankoXml::ConfigurationError, "validation_data ocsp_responses: must be DER OCSPResponses"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :parse_ocsp_response
|
|
81
|
+
|
|
82
|
+
# Appends <xades:CertificateValues> and <xades:RevocationValues> (each
|
|
83
|
+
# only when it has entries) to +parent+, an UnsignedSignatureProperties
|
|
84
|
+
# in +namespace+.
|
|
85
|
+
def append_to(parent, namespace)
|
|
86
|
+
add = lambda do |node, name|
|
|
87
|
+
child = parent.document.create_element(name)
|
|
88
|
+
node.add_child(child)
|
|
89
|
+
child.namespace = namespace
|
|
90
|
+
child
|
|
91
|
+
end
|
|
92
|
+
encapsulate = ->(node, name, der) { add.call(node, name).content = [der].pack("m0") }
|
|
93
|
+
|
|
94
|
+
unless certificates.empty?
|
|
95
|
+
values = add.call(parent, "CertificateValues")
|
|
96
|
+
certificates.each do |certificate|
|
|
97
|
+
encapsulate.call(values, "EncapsulatedX509Certificate", certificate.to_der)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
return if crls.empty? && ocsp_responses.empty?
|
|
101
|
+
|
|
102
|
+
# RevocationValuesType is a sequence: CRLValues, then OCSPValues.
|
|
103
|
+
revocation = add.call(parent, "RevocationValues")
|
|
104
|
+
unless crls.empty?
|
|
105
|
+
values = add.call(revocation, "CRLValues")
|
|
106
|
+
crls.each { |crl| encapsulate.call(values, "EncapsulatedCRLValue", crl.to_der) }
|
|
107
|
+
end
|
|
108
|
+
return if ocsp_responses.empty?
|
|
109
|
+
|
|
110
|
+
values = add.call(revocation, "OCSPValues")
|
|
111
|
+
ocsp_responses.each { |response| encapsulate.call(values, "EncapsulatedOCSPValue", response.to_der) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|