hanko_xml 0.1.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 +7 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE.txt +21 -0
- data/README.md +222 -0
- data/THIRD_PARTY_LICENSES.md +392 -0
- data/dependencies.yml +87 -0
- 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/algorithms.rb +228 -0
- data/lib/hanko_xml/dom.rb +137 -0
- data/lib/hanko_xml/errors.rb +132 -0
- data/lib/hanko_xml/exc-c14n.xsd +26 -0
- data/lib/hanko_xml/input.rb +50 -0
- data/lib/hanko_xml/key.rb +213 -0
- data/lib/hanko_xml/key_info.rb +67 -0
- data/lib/hanko_xml/native_version.rb +75 -0
- data/lib/hanko_xml/nokogiri_ext.rb +34 -0
- data/lib/hanko_xml/placement.rb +127 -0
- data/lib/hanko_xml/policy.rb +502 -0
- data/lib/hanko_xml/reference.rb +49 -0
- data/lib/hanko_xml/signature_info.rb +134 -0
- data/lib/hanko_xml/signed_document.rb +155 -0
- data/lib/hanko_xml/signer.rb +572 -0
- data/lib/hanko_xml/template.rb +150 -0
- data/lib/hanko_xml/transform.rb +87 -0
- data/lib/hanko_xml/verification.rb +377 -0
- data/lib/hanko_xml/verifier.rb +694 -0
- data/lib/hanko_xml/version.rb +5 -0
- data/lib/hanko_xml/xades/verifier.rb +319 -0
- data/lib/hanko_xml/xades.rb +287 -0
- data/lib/hanko_xml/xml.rb +93 -0
- data/lib/hanko_xml/xmldsig-core-schema.xsd +332 -0
- data/lib/hanko_xml.rb +122 -0
- data/sig/hanko_xml/key.rbs +48 -0
- data/sig/hanko_xml/native.rbs +12 -0
- data/sig/hanko_xml/native_version.rbs +13 -0
- data/sig/hanko_xml/nokogiri_ext.rbs +23 -0
- data/sig/hanko_xml/placement.rbs +27 -0
- data/sig/hanko_xml/policy.rbs +83 -0
- data/sig/hanko_xml/reference.rbs +36 -0
- data/sig/hanko_xml/signed_document.rbs +66 -0
- data/sig/hanko_xml/signer.rbs +50 -0
- data/sig/hanko_xml/template.rbs +41 -0
- data/sig/hanko_xml/verification.rbs +100 -0
- data/sig/hanko_xml/verifier.rbs +45 -0
- data/sig/hanko_xml/xades.rbs +70 -0
- data/sig/hanko_xml.rbs +62 -0
- metadata +104 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module HankoXml
|
|
7
|
+
module Xades
|
|
8
|
+
# A {HankoXml::Verification} that also carries the XAdES signed
|
|
9
|
+
# properties. #valid?, #signed? and #at_signed are false/raise whenever
|
|
10
|
+
# the XAdES checks failed, not only when XML-DSig did.
|
|
11
|
+
class Verification < HankoXml::Verification
|
|
12
|
+
# @return [SignedProperties, nil] Only when #valid?.
|
|
13
|
+
attr_reader :signed_properties
|
|
14
|
+
|
|
15
|
+
def initialize(signed_properties: nil, **attributes)
|
|
16
|
+
@signed_properties = signed_properties
|
|
17
|
+
super(**attributes)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Verifies a XAdES-B signature (#58): the XML-DSig signature through
|
|
22
|
+
# {HankoXml::Verifier}, then its <xades:SignedProperties>.
|
|
23
|
+
#
|
|
24
|
+
# Everything read comes from the SignedProperties element that xmlsec
|
|
25
|
+
# itself resolved and digested for the Type=SignedProperties Reference, and
|
|
26
|
+
# that element must be the one inside this signature's single
|
|
27
|
+
# QualifyingProperties. A decoy SignedProperties placed where a lookup by
|
|
28
|
+
# XPath would find it is never read (XAdES signature wrapping). The
|
|
29
|
+
# SigningCertificate(V2) must then name the certificate that verified.
|
|
30
|
+
#
|
|
31
|
+
# Reads XAdES 1.3.2 and the older 1.2.2 and 1.1.1 namespaces (#86); the
|
|
32
|
+
# whole signature must use one.
|
|
33
|
+
#
|
|
34
|
+
# Needs certificate-based trust (certificate:, certificates:, store:), and
|
|
35
|
+
# Nokogiri: the signed properties are read from the DOM (#102).
|
|
36
|
+
# Unsigned properties (XAdES-T/LT/LTA) are ignored: #81, #82.
|
|
37
|
+
class Verifier < HankoXml::Verifier # rubocop:disable Metrics/ClassLength
|
|
38
|
+
# @param signature_policy [SignaturePolicy, Hash, :implied, nil] When given, the signed
|
|
39
|
+
# SignaturePolicyIdentifier must be exactly this.
|
|
40
|
+
# All other keywords are {HankoXml::Verifier}'s.
|
|
41
|
+
def initialize(signature_policy: nil, **options)
|
|
42
|
+
Dom.require!("HankoXml::Xades::Verifier")
|
|
43
|
+
if options[:key] || options[:keys]
|
|
44
|
+
raise HankoXml::ConfigurationError,
|
|
45
|
+
"XAdES verification needs certificate:, certificates: or store:, since the signed properties " \
|
|
46
|
+
"commit to a certificate"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@expected_signature_policy = coerce_signature_policy(signature_policy)
|
|
50
|
+
super(**options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The signed properties commit to a certificate, so XAdES needs trust in
|
|
54
|
+
# it; there is no integrity-only XAdES verification (#103).
|
|
55
|
+
def self.without_trust_verification(**)
|
|
56
|
+
raise HankoXml::ConfigurationError,
|
|
57
|
+
"XAdES verification needs certificate:, certificates: or store:; " \
|
|
58
|
+
"without_trust_verification is not available for it"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def verify(xml)
|
|
62
|
+
xades(super)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def verify_all(xml)
|
|
66
|
+
super.map { |verification| xades(verification) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def xades(verification)
|
|
72
|
+
return rebuild(verification) unless verification.valid?
|
|
73
|
+
|
|
74
|
+
properties = signed_properties(verification)
|
|
75
|
+
rebuild(verification, signed_properties: properties)
|
|
76
|
+
rescue HankoXml::XadesError, HankoXml::PolicyError => e
|
|
77
|
+
rebuild(verification, error: e)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rebuild(verification, signed_properties: nil, error: nil)
|
|
81
|
+
Verification.new(
|
|
82
|
+
valid: verification.valid? && error.nil?,
|
|
83
|
+
errors: [*verification.errors, *error],
|
|
84
|
+
signature_info: verification.signature_info,
|
|
85
|
+
signature_node: verification.signature_node,
|
|
86
|
+
document: verification.document,
|
|
87
|
+
namespaces: verification.namespaces,
|
|
88
|
+
certificate: verification.certificate,
|
|
89
|
+
certificate_chain: verification.certificate_chain,
|
|
90
|
+
trusted: verification.trusted?,
|
|
91
|
+
root_signature: verification.send(:root_signature),
|
|
92
|
+
id_attributes: verification.id_attributes,
|
|
93
|
+
resolver: verification.resolver,
|
|
94
|
+
reference_element_indexes: verification.reference_element_indexes,
|
|
95
|
+
reference_valid: verification.reference_valid,
|
|
96
|
+
signature_element_index: verification.signature_element_index,
|
|
97
|
+
signed_properties: signed_properties
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def signed_properties(verification)
|
|
102
|
+
node, ns = signed_properties_node(verification)
|
|
103
|
+
signature_properties = node.at_xpath("xades:SignedSignatureProperties", ns)
|
|
104
|
+
fail!("<xades:SignedProperties> has no <xades:SignedSignatureProperties>") unless signature_properties
|
|
105
|
+
|
|
106
|
+
check_signing_certificate!(signature_properties, verification.certificate, ns)
|
|
107
|
+
policy = signature_policy(signature_properties, ns)
|
|
108
|
+
check_signature_policy!(policy)
|
|
109
|
+
|
|
110
|
+
SignedProperties.new(
|
|
111
|
+
signing_time: signing_time(signature_properties, ns),
|
|
112
|
+
signature_policy: policy,
|
|
113
|
+
claimed_roles: signature_properties.xpath("xades:SignerRoleV2/xades:ClaimedRoles/xades:ClaimedRole | " \
|
|
114
|
+
"xades:SignerRole/xades:ClaimedRoles/xades:ClaimedRole", ns)
|
|
115
|
+
.map { |role| role.text.strip.freeze }.freeze,
|
|
116
|
+
data_object_formats: data_object_formats(node, ns)
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The SignedProperties xmlsec digested, checked to be the one this
|
|
121
|
+
# signature's QualifyingProperties holds, and the namespaces to read it
|
|
122
|
+
# with.
|
|
123
|
+
def signed_properties_node(verification)
|
|
124
|
+
info, reference = signed_properties_reference(verification)
|
|
125
|
+
qualifying, ns = qualifying_properties(verification.signature_node)
|
|
126
|
+
|
|
127
|
+
type_namespace = VERSIONED_SIGNED_PROPERTIES_TYPES[info.type]
|
|
128
|
+
if type_namespace && type_namespace != ns["xades"]
|
|
129
|
+
fail!("the SignedProperties <Reference> Type names another XAdES version than its QualifyingProperties")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
expected = qualifying.xpath("xades:SignedProperties", ns)
|
|
133
|
+
unless expected.size == 1 && expected.first.pointer_id == reference.node.pointer_id
|
|
134
|
+
fail!("the signed <xades:SignedProperties> is not the one in this signature's QualifyingProperties")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
[reference.node, ns]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The single SignedProperties Reference, as SignedInfo claims it and as
|
|
141
|
+
# it verified.
|
|
142
|
+
def signed_properties_reference(verification)
|
|
143
|
+
pairs = verification.signature_info.references.zip(verification.references)
|
|
144
|
+
.select { |info, reference| signed_properties_reference?(info, reference) }
|
|
145
|
+
unless pairs.size == 1
|
|
146
|
+
fail!("a XAdES signature has exactly one <Reference> of Type #{SIGNED_PROPERTIES_TYPE}, " \
|
|
147
|
+
"this one has #{pairs.size}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
reference = pairs.first.last
|
|
151
|
+
fail!("the SignedProperties <Reference> did not verify") unless reference.valid? && reference.node
|
|
152
|
+
|
|
153
|
+
pairs.first
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# This signature's single QualifyingProperties, in any XAdES namespace,
|
|
157
|
+
# and the namespaces to read it with.
|
|
158
|
+
def qualifying_properties(signature)
|
|
159
|
+
signature_id = signature["Id"].to_s
|
|
160
|
+
fail!("the <Signature> has no Id for QualifyingProperties to target") if signature_id.empty?
|
|
161
|
+
|
|
162
|
+
qualifying = signature.xpath("ds:Object/*[local-name()='QualifyingProperties']", "ds" => HankoXml::NS["ds"])
|
|
163
|
+
.select { |node| NAMESPACES.include?(node.namespace&.href) }
|
|
164
|
+
fail!("a XAdES signature has exactly one <xades:QualifyingProperties>") unless qualifying.size == 1
|
|
165
|
+
|
|
166
|
+
qualifying = qualifying.first
|
|
167
|
+
fail!("<xades:QualifyingProperties> does not target this <Signature>") unless targets?(qualifying, signature_id)
|
|
168
|
+
|
|
169
|
+
[qualifying, { "ds" => HankoXml::NS["ds"], "xades" => qualifying.namespace.href }]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# By its Type, or, with no Type (as some 1.2.2 signatures write it), by
|
|
173
|
+
# what xmlsec resolved and digested: a SignedProperties element in a
|
|
174
|
+
# XAdES namespace. Never by the URI text (#86).
|
|
175
|
+
def signed_properties_reference?(info, reference)
|
|
176
|
+
return info.type == SIGNED_PROPERTIES_TYPE || VERSIONED_SIGNED_PROPERTIES_TYPES.key?(info.type) if info.type
|
|
177
|
+
|
|
178
|
+
node = reference.node
|
|
179
|
+
!node.nil? && node.name == "SignedProperties" && NAMESPACES.include?(node.namespace&.href)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Target is "#" + the Signature Id. The older namespaces also had
|
|
183
|
+
# signers write the bare Id (Target="Signature"). Either way the
|
|
184
|
+
# QualifyingProperties must sit in this <Signature>'s own <Object>.
|
|
185
|
+
def targets?(qualifying, signature_id)
|
|
186
|
+
target = qualifying["Target"]
|
|
187
|
+
return true if target == "##{signature_id}"
|
|
188
|
+
|
|
189
|
+
LEGACY_NAMESPACES.include?(qualifying.namespace.href) && target == signature_id
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def check_signing_certificate!(signature_properties, certificate, ns)
|
|
193
|
+
fail!("no certificate verified the signature, so SigningCertificate cannot be checked") unless certificate
|
|
194
|
+
|
|
195
|
+
v2 = signature_properties.xpath("xades:SigningCertificateV2/xades:Cert", ns)
|
|
196
|
+
v1 = signature_properties.xpath("xades:SigningCertificate/xades:Cert", ns)
|
|
197
|
+
if v1.empty? == v2.empty?
|
|
198
|
+
fail!("exactly one of <xades:SigningCertificate> or <xades:SigningCertificateV2> is required")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
cert = (v2.empty? ? v1 : v2).find { |candidate| certificate_digest_matches?(candidate, certificate, ns) }
|
|
202
|
+
fail!("SigningCertificate does not name the certificate that verified the signature") unless cert
|
|
203
|
+
|
|
204
|
+
check_issuer_serial!(cert, certificate, ns)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def certificate_digest_matches?(cert, certificate, ns)
|
|
208
|
+
digest_method = cert.at_xpath("xades:CertDigest/ds:DigestMethod/@Algorithm", ns)&.value
|
|
209
|
+
digest_value = cert.at_xpath("xades:CertDigest/ds:DigestValue", ns)&.text
|
|
210
|
+
fail!("a <xades:Cert> has no CertDigest") unless digest_method && digest_value
|
|
211
|
+
|
|
212
|
+
@policy.enforce_algorithm!(:digest, digest_method)
|
|
213
|
+
name = HankoXml::Algorithms.name_for(:digest, digest_method)
|
|
214
|
+
actual = Xades.openssl_digest(name, certificate.to_der)
|
|
215
|
+
decoded = digest_value.gsub(/\s/, "").unpack1("m")
|
|
216
|
+
OpenSSL.fixed_length_secure_compare(actual, decoded) if actual.bytesize == decoded.bytesize
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# IssuerSerialV2 must decode to the certificate's issuer and serial.
|
|
220
|
+
# The v1 IssuerSerial's issuer is a DN string whose formatting varies
|
|
221
|
+
# between implementations, so only its serial number is compared.
|
|
222
|
+
def check_issuer_serial!(cert, certificate, ns)
|
|
223
|
+
v2 = cert.at_xpath("xades:IssuerSerialV2", ns)&.text
|
|
224
|
+
v1 = cert.at_xpath("xades:IssuerSerial/ds:X509SerialNumber", ns)&.text
|
|
225
|
+
|
|
226
|
+
if v2
|
|
227
|
+
issuer, serial = decode_issuer_serial(v2)
|
|
228
|
+
return if issuer == certificate.issuer && serial == certificate.serial
|
|
229
|
+
|
|
230
|
+
fail!("IssuerSerialV2 does not match the certificate that verified the signature")
|
|
231
|
+
elsif v1
|
|
232
|
+
fail!("the IssuerSerial of <xades:SigningCertificate> is malformed") unless v1.strip.match?(/\A\d+\z/)
|
|
233
|
+
return if OpenSSL::BN.new(v1.strip) == certificate.serial
|
|
234
|
+
|
|
235
|
+
fail!("IssuerSerial does not match the certificate that verified the signature")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# RFC 5035 IssuerSerial: SEQUENCE { GeneralNames, INTEGER }, the issuer
|
|
240
|
+
# being the GeneralNames' directoryName [4].
|
|
241
|
+
def decode_issuer_serial(base64)
|
|
242
|
+
sequence = OpenSSL::ASN1.decode(base64.gsub(/\s/, "").unpack1("m"))
|
|
243
|
+
general_names, serial = sequence.value if sequence.is_a?(OpenSSL::ASN1::Sequence)
|
|
244
|
+
names = general_names.is_a?(OpenSSL::ASN1::Sequence) ? general_names.value : []
|
|
245
|
+
directory_name = names.find { |name| name.tag == 4 && name.tag_class == :CONTEXT_SPECIFIC }
|
|
246
|
+
name = directory_name&.value&.first
|
|
247
|
+
unless name && serial.is_a?(OpenSSL::ASN1::Integer)
|
|
248
|
+
fail!("the IssuerSerialV2 of <xades:SigningCertificateV2> is malformed")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
[OpenSSL::X509::Name.new(name.to_der), serial.value]
|
|
252
|
+
rescue OpenSSL::ASN1::ASN1Error, OpenSSL::X509::NameError
|
|
253
|
+
fail!("the IssuerSerialV2 of <xades:SigningCertificateV2> is malformed")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def signing_time(signature_properties, ns)
|
|
257
|
+
text = signature_properties.at_xpath("xades:SigningTime", ns)&.text
|
|
258
|
+
return nil unless text
|
|
259
|
+
|
|
260
|
+
Time.iso8601(text.strip).utc.freeze
|
|
261
|
+
rescue ArgumentError
|
|
262
|
+
fail!("<xades:SigningTime> is not an xs:dateTime")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def signature_policy(signature_properties, ns)
|
|
266
|
+
identifier = signature_properties.at_xpath("xades:SignaturePolicyIdentifier", ns)
|
|
267
|
+
return nil unless identifier
|
|
268
|
+
return :implied if identifier.at_xpath("xades:SignaturePolicyImplied", ns)
|
|
269
|
+
|
|
270
|
+
policy_id = identifier.at_xpath("xades:SignaturePolicyId", ns)
|
|
271
|
+
SignaturePolicy.new(
|
|
272
|
+
identifier: policy_id&.at_xpath("xades:SigPolicyId/xades:Identifier", ns)&.text.to_s.strip,
|
|
273
|
+
description: policy_id&.at_xpath("xades:SigPolicyId/xades:Description", ns)&.text,
|
|
274
|
+
digest_method: policy_id&.at_xpath("xades:SigPolicyHash/ds:DigestMethod/@Algorithm", ns)&.value.to_s,
|
|
275
|
+
digest_value: policy_id&.at_xpath("xades:SigPolicyHash/ds:DigestValue", ns)&.text.to_s.gsub(/\s/, "")
|
|
276
|
+
)
|
|
277
|
+
rescue HankoXml::ConfigurationError
|
|
278
|
+
fail!("<xades:SignaturePolicyIdentifier> is malformed")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def check_signature_policy!(policy)
|
|
282
|
+
expected = @expected_signature_policy
|
|
283
|
+
return if expected.nil?
|
|
284
|
+
return if expected == :implied && policy == :implied
|
|
285
|
+
return if policy.is_a?(SignaturePolicy) && expected.is_a?(SignaturePolicy) &&
|
|
286
|
+
policy.identifier == expected.identifier &&
|
|
287
|
+
policy.digest_value == expected.digest_value &&
|
|
288
|
+
policy.digest_method == HankoXml::Algorithms.uri_for(:digest, expected.digest_method)
|
|
289
|
+
|
|
290
|
+
fail!("the signed SignaturePolicyIdentifier is not the expected signature policy")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def data_object_formats(signed_properties, ns)
|
|
294
|
+
signed_properties.xpath("xades:SignedDataObjectProperties/xades:DataObjectFormat", ns).map do |format|
|
|
295
|
+
{
|
|
296
|
+
object_reference: format["ObjectReference"],
|
|
297
|
+
mime_type: format.at_xpath("xades:MimeType", ns)&.text,
|
|
298
|
+
description: format.at_xpath("xades:Description", ns)&.text,
|
|
299
|
+
encoding: format.at_xpath("xades:Encoding", ns)&.text
|
|
300
|
+
}.freeze
|
|
301
|
+
end.freeze
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def coerce_signature_policy(value)
|
|
305
|
+
case value
|
|
306
|
+
when nil, :implied, SignaturePolicy then value
|
|
307
|
+
when Hash then SignaturePolicy.new(**value)
|
|
308
|
+
else
|
|
309
|
+
raise HankoXml::ConfigurationError,
|
|
310
|
+
"signature_policy: must be a Xades::SignaturePolicy, a Hash of its keywords, :implied or nil"
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def fail!(message)
|
|
315
|
+
raise HankoXml::XadesError, message
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module HankoXml
|
|
8
|
+
# XAdES (ETSI EN 319 132-1) baseline signatures: XML-DSig plus signed
|
|
9
|
+
# qualifying properties. Only structure lives here. xmlsec digests the
|
|
10
|
+
# SignedProperties Reference and signs, exactly as for any other same-document
|
|
11
|
+
# Reference. See API_CONTRACT.md §12 and docs/src/content/guide/comparison-signxml.md.
|
|
12
|
+
module Xades
|
|
13
|
+
NS = "http://uri.etsi.org/01903/v1.3.2#"
|
|
14
|
+
|
|
15
|
+
# Earlier XAdES namespaces (TS 101 903 v1.1.1, v1.2.2) that Xades::Verifier
|
|
16
|
+
# also reads (#86). Xades::Signer writes only NS.
|
|
17
|
+
LEGACY_NAMESPACES = %w[http://uri.etsi.org/01903/v1.2.2# http://uri.etsi.org/01903/v1.1.1#].freeze
|
|
18
|
+
|
|
19
|
+
# Every namespace Xades::Verifier reads QualifyingProperties in.
|
|
20
|
+
NAMESPACES = [NS, *LEGACY_NAMESPACES].freeze
|
|
21
|
+
|
|
22
|
+
# RFC 5035 IssuerSerial: issuer as GeneralNames { directoryName [4] Name }.
|
|
23
|
+
def self.issuer_serial_der(certificate)
|
|
24
|
+
name = OpenSSL::ASN1.decode(certificate.issuer.to_der)
|
|
25
|
+
general_names = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ASN1Data.new([name], 4, :CONTEXT_SPECIFIC)])
|
|
26
|
+
OpenSSL::ASN1::Sequence([general_names, OpenSSL::ASN1::Integer(certificate.serial)]).to_der
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Digests a certificate for <xades:CertDigest> with the registry digest
|
|
30
|
+
# +name+. OpenSSL spells SHA-3 "sha3-256" where the registry has
|
|
31
|
+
# :sha3_256 (#60).
|
|
32
|
+
def self.openssl_digest(name, bytes)
|
|
33
|
+
OpenSSL::Digest.new(name.to_s.tr("_", "-")).digest(bytes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The Reference Type that marks the Reference over <xades:SignedProperties>.
|
|
37
|
+
SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties"
|
|
38
|
+
|
|
39
|
+
# The Types a verified signature may use instead: the namespace-specific
|
|
40
|
+
# form older signatures write (http://uri.etsi.org/01903/v1.2.2#SignedProperties).
|
|
41
|
+
# Each only counts with QualifyingProperties in its own namespace (#86).
|
|
42
|
+
VERSIONED_SIGNED_PROPERTIES_TYPES = NAMESPACES.to_h { |ns| ["#{ns}SignedProperties", ns] }.freeze
|
|
43
|
+
|
|
44
|
+
# <xades:SignaturePolicyIdentifier>: the policy document's identifier (an
|
|
45
|
+
# OID URN or URI) and the digest of that document.
|
|
46
|
+
SignaturePolicy = Struct.new(:identifier, :digest_value, :digest_method, :description, keyword_init: true) do
|
|
47
|
+
def initialize(identifier:, digest_value:, digest_method: :sha256, description: nil)
|
|
48
|
+
begin
|
|
49
|
+
digest_value.to_s.unpack1("m0")
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
raise HankoXml::ConfigurationError,
|
|
52
|
+
"signature_policy digest_value: must be Base64 (the policy document's digest)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
super
|
|
56
|
+
freeze
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# <xades:DataObjectFormat> for the signed data.
|
|
61
|
+
DataObjectFormat = Struct.new(:mime_type, :description, :encoding, keyword_init: true) do
|
|
62
|
+
def initialize(mime_type: nil, description: nil, encoding: nil)
|
|
63
|
+
if [mime_type, description, encoding].all?(&:nil?)
|
|
64
|
+
raise HankoXml::ConfigurationError, "data_object_format: needs a mime_type:, description: or encoding:"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
super
|
|
68
|
+
freeze
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The authenticated view of a verified <xades:SignedProperties>
|
|
73
|
+
# (Xades::Verification#signed_properties). Unsigned properties
|
|
74
|
+
# (timestamps, validation data) are never read.
|
|
75
|
+
SignedProperties = Struct.new(:signing_time, :signature_policy, :claimed_roles, :data_object_formats,
|
|
76
|
+
keyword_init: true) do
|
|
77
|
+
def initialize(**)
|
|
78
|
+
super
|
|
79
|
+
freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A {HankoXml::Signer} that produces XAdES-B (baseline) signatures (#57).
|
|
84
|
+
#
|
|
85
|
+
# On top of the Signer's own references it adds a <ds:Object> holding
|
|
86
|
+
# <xades:QualifyingProperties>, and a Reference over its
|
|
87
|
+
# <xades:SignedProperties>: SigningTime, SigningCertificateV2 (digest of
|
|
88
|
+
# certificate:), and optionally SignaturePolicyIdentifier, SignerRoleV2
|
|
89
|
+
# and DataObjectFormat.
|
|
90
|
+
#
|
|
91
|
+
# Always builds a new <Signature> (template: :build). Timestamps and
|
|
92
|
+
# validation data (XAdES-T/LT/LTA) are separate: #81, #82.
|
|
93
|
+
class Signer < HankoXml::Signer
|
|
94
|
+
attr_reader :signing_time, :signature_policy, :claimed_roles, :data_object_format
|
|
95
|
+
|
|
96
|
+
# @param certificate [Key, OpenSSL::X509::Certificate, String, Pathname] Required: SigningCertificateV2
|
|
97
|
+
# commits to it.
|
|
98
|
+
# @param signing_time [Time, nil] The SigningTime to sign; nil means the time #sign runs.
|
|
99
|
+
# @param signature_policy [SignaturePolicy, Hash, :implied, nil] SignaturePolicyIdentifier, if any.
|
|
100
|
+
# @param claimed_roles [Array<String>] SignerRoleV2/ClaimedRoles.
|
|
101
|
+
# @param data_object_format [DataObjectFormat, Hash, nil] Describes the signed data; applies to each
|
|
102
|
+
# of this Signer's own references, which get an Id when they have none.
|
|
103
|
+
# @param signature_id [String, nil] The <Signature> Id; generated when nil, since QualifyingProperties
|
|
104
|
+
# targets it.
|
|
105
|
+
# All other keywords are {HankoXml::Signer}'s.
|
|
106
|
+
def initialize(certificate: nil, signing_time: nil, signature_policy: nil, claimed_roles: [],
|
|
107
|
+
data_object_format: nil, signature_id: nil, template: :build, **options, &block)
|
|
108
|
+
unless template.to_sym == :build
|
|
109
|
+
raise HankoXml::ConfigurationError,
|
|
110
|
+
"template: #{template.inspect} is not supported for XAdES; it always builds a new <Signature>"
|
|
111
|
+
end
|
|
112
|
+
if signing_time && !signing_time.respond_to?(:utc)
|
|
113
|
+
raise HankoXml::ConfigurationError, "signing_time: must be a Time, got #{signing_time.class}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@signing_time = signing_time&.dup&.utc&.freeze
|
|
117
|
+
@signature_policy = coerce_signature_policy(signature_policy)
|
|
118
|
+
@claimed_roles = Array(claimed_roles).map { |role| role.to_s.dup.freeze }.freeze
|
|
119
|
+
@data_object_format = coerce_data_object_format(data_object_format)
|
|
120
|
+
|
|
121
|
+
super(certificate: certificate, signature_id: signature_id || "xades-#{SecureRandom.hex(8)}",
|
|
122
|
+
template: :build, **options, &block)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# This Signer's own references (the whole document when it has none),
|
|
126
|
+
# then the Reference over SignedProperties.
|
|
127
|
+
def references
|
|
128
|
+
own = super
|
|
129
|
+
own = [HankoXml::Reference.new(uri: "", digest_method: digest_method)] if own.empty?
|
|
130
|
+
own = own.each_with_index.map { |ref, i| with_id(ref, i) } if data_object_format
|
|
131
|
+
[*own, signed_properties_reference]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The <Signature> the Signer would build, with <xades:QualifyingProperties>.
|
|
135
|
+
def template(doc_or_xml)
|
|
136
|
+
signature = super
|
|
137
|
+
signature.add_child(QualifyingProperties.new(signature, self).build)
|
|
138
|
+
signature
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def signed_properties_id
|
|
142
|
+
"#{signature_id}-signedprops"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def signed_properties_reference
|
|
148
|
+
HankoXml::Reference.new(uri: "##{signed_properties_id}", type: SIGNED_PROPERTIES_TYPE,
|
|
149
|
+
transforms: [:exc_c14n], digest_method: digest_method)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def with_id(ref, index)
|
|
153
|
+
return ref if ref.id
|
|
154
|
+
|
|
155
|
+
HankoXml::Reference.new(uri: ref.uri, transforms: ref.transforms, digest_method: ref.digest_method,
|
|
156
|
+
id: "#{signature_id}-ref-#{index}", type: ref.type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def coerce_signature_policy(value)
|
|
160
|
+
case value
|
|
161
|
+
when nil, :implied, SignaturePolicy then value
|
|
162
|
+
when Hash then SignaturePolicy.new(**value)
|
|
163
|
+
else
|
|
164
|
+
raise HankoXml::ConfigurationError,
|
|
165
|
+
"signature_policy: must be a Xades::SignaturePolicy, a Hash of its keywords, :implied or nil"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def coerce_data_object_format(value)
|
|
170
|
+
case value
|
|
171
|
+
when nil, DataObjectFormat then value
|
|
172
|
+
when Hash then DataObjectFormat.new(**value)
|
|
173
|
+
else
|
|
174
|
+
raise HankoXml::ConfigurationError,
|
|
175
|
+
"data_object_format: must be a Xades::DataObjectFormat, a Hash of its keywords or nil"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Checked with the key material, after the block may have set certificate:.
|
|
180
|
+
def check_key_material!
|
|
181
|
+
super
|
|
182
|
+
return if certificate.is_a?(HankoXml::Key) && certificate.certificate?
|
|
183
|
+
|
|
184
|
+
raise HankoXml::ConfigurationError, "XAdES needs certificate:, which SigningCertificateV2 commits to"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Builds <ds:Object><xades:QualifyingProperties> for a <Signature> template.
|
|
189
|
+
# Every node gets its namespace explicitly, so the caller's DOM (sign!)
|
|
190
|
+
# answers XPath the same way a re-parse of the signed bytes would.
|
|
191
|
+
class QualifyingProperties
|
|
192
|
+
def initialize(signature, signer)
|
|
193
|
+
@doc = signature.document
|
|
194
|
+
@ds_namespace = signature.namespace
|
|
195
|
+
@signer = signer
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build
|
|
199
|
+
object = element("Object")
|
|
200
|
+
qualifying = @doc.create_element("QualifyingProperties")
|
|
201
|
+
object.add_child(qualifying)
|
|
202
|
+
@xades_ns = qualifying.add_namespace_definition("xades", NS)
|
|
203
|
+
qualifying.namespace = @xades_ns
|
|
204
|
+
qualifying["Target"] = "##{@signer.signature_id}"
|
|
205
|
+
signed = xades(qualifying, "SignedProperties")
|
|
206
|
+
signed["Id"] = @signer.signed_properties_id
|
|
207
|
+
|
|
208
|
+
build_signature_properties(xades(signed, "SignedSignatureProperties"))
|
|
209
|
+
build_data_object_properties(signed) if @signer.data_object_format
|
|
210
|
+
object
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def build_signature_properties(parent)
|
|
216
|
+
xades(parent, "SigningTime").content = (@signer.signing_time || Time.now.utc).utc.xmlschema
|
|
217
|
+
build_signing_certificate(xades(parent, "SigningCertificateV2"))
|
|
218
|
+
build_signature_policy(parent) if @signer.signature_policy
|
|
219
|
+
return if @signer.claimed_roles.empty?
|
|
220
|
+
|
|
221
|
+
roles = xades(xades(parent, "SignerRoleV2"), "ClaimedRoles")
|
|
222
|
+
@signer.claimed_roles.each { |role| xades(roles, "ClaimedRole").content = role }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def build_signing_certificate(parent)
|
|
226
|
+
certificate = @signer.certificate.to_openssl
|
|
227
|
+
cert = xades(parent, "Cert")
|
|
228
|
+
digest(xades(cert, "CertDigest"), @signer.digest_method, certificate.to_der)
|
|
229
|
+
xades(cert, "IssuerSerialV2").content = [issuer_serial_der(certificate)].pack("m0")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def issuer_serial_der(certificate)
|
|
233
|
+
Xades.issuer_serial_der(certificate)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_signature_policy(parent)
|
|
237
|
+
identifier = xades(parent, "SignaturePolicyIdentifier")
|
|
238
|
+
policy = @signer.signature_policy
|
|
239
|
+
return xades(identifier, "SignaturePolicyImplied") if policy == :implied
|
|
240
|
+
|
|
241
|
+
policy_id = xades(identifier, "SignaturePolicyId")
|
|
242
|
+
sig_policy_id = xades(policy_id, "SigPolicyId")
|
|
243
|
+
xades(sig_policy_id, "Identifier").content = policy.identifier
|
|
244
|
+
xades(sig_policy_id, "Description").content = policy.description if policy.description
|
|
245
|
+
hash = xades(policy_id, "SigPolicyHash")
|
|
246
|
+
hash.add_child(digest_method_element(policy.digest_method))
|
|
247
|
+
hash.add_child(element("DigestValue")).content = policy.digest_value
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def build_data_object_properties(signed)
|
|
251
|
+
properties = xades(signed, "SignedDataObjectProperties")
|
|
252
|
+
format = @signer.data_object_format
|
|
253
|
+
@signer.references.reject { |ref| ref.type == SIGNED_PROPERTIES_TYPE }.each do |ref|
|
|
254
|
+
node = xades(properties, "DataObjectFormat")
|
|
255
|
+
node["ObjectReference"] = "##{ref.id}"
|
|
256
|
+
xades(node, "Description").content = format.description if format.description
|
|
257
|
+
xades(node, "MimeType").content = format.mime_type if format.mime_type
|
|
258
|
+
xades(node, "Encoding").content = format.encoding if format.encoding
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def digest(parent, digest_method, bytes)
|
|
263
|
+
parent.add_child(digest_method_element(digest_method))
|
|
264
|
+
algorithm = HankoXml::Algorithms.name_for(:digest, HankoXml::Algorithms.uri_for(:digest, digest_method))
|
|
265
|
+
parent.add_child(element("DigestValue")).content = [Xades.openssl_digest(algorithm, bytes)].pack("m0")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def digest_method_element(digest_method)
|
|
269
|
+
element("DigestMethod").tap { |node| node["Algorithm"] = HankoXml::Algorithms.uri_for(:digest, digest_method) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def xades(parent, name)
|
|
273
|
+
node = @doc.create_element(name)
|
|
274
|
+
parent.add_child(node)
|
|
275
|
+
node.namespace = @xades_ns
|
|
276
|
+
node
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# An XML-DSig element, in the default namespace <Signature> declares.
|
|
280
|
+
def element(name)
|
|
281
|
+
@doc.create_element(name).tap { |node| node.namespace = @ds_namespace }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
require_relative "xades/verifier"
|