hanko_xml 0.1.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +44 -0
  3. data/Cargo.lock +401 -0
  4. data/Cargo.toml +7 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +222 -0
  7. data/THIRD_PARTY_LICENSES.md +392 -0
  8. data/dependencies.yml +87 -0
  9. data/ext/hanko_xml/Cargo.toml +20 -0
  10. data/ext/hanko_xml/build/vendor.rs +817 -0
  11. data/ext/hanko_xml/build.rs +298 -0
  12. data/ext/hanko_xml/extconf.rb +6 -0
  13. data/ext/hanko_xml/src/dsig.rs +1851 -0
  14. data/ext/hanko_xml/src/errors.rs +151 -0
  15. data/ext/hanko_xml/src/lib.rs +490 -0
  16. data/ext/hanko_xml/src/sys.rs +43 -0
  17. data/lib/hanko_xml/algorithms.rb +228 -0
  18. data/lib/hanko_xml/dom.rb +137 -0
  19. data/lib/hanko_xml/errors.rb +132 -0
  20. data/lib/hanko_xml/exc-c14n.xsd +26 -0
  21. data/lib/hanko_xml/input.rb +50 -0
  22. data/lib/hanko_xml/key.rb +213 -0
  23. data/lib/hanko_xml/key_info.rb +67 -0
  24. data/lib/hanko_xml/native_version.rb +75 -0
  25. data/lib/hanko_xml/nokogiri_ext.rb +34 -0
  26. data/lib/hanko_xml/placement.rb +127 -0
  27. data/lib/hanko_xml/policy.rb +502 -0
  28. data/lib/hanko_xml/reference.rb +49 -0
  29. data/lib/hanko_xml/signature_info.rb +134 -0
  30. data/lib/hanko_xml/signed_document.rb +155 -0
  31. data/lib/hanko_xml/signer.rb +572 -0
  32. data/lib/hanko_xml/template.rb +150 -0
  33. data/lib/hanko_xml/transform.rb +87 -0
  34. data/lib/hanko_xml/verification.rb +377 -0
  35. data/lib/hanko_xml/verifier.rb +694 -0
  36. data/lib/hanko_xml/version.rb +5 -0
  37. data/lib/hanko_xml/xades/verifier.rb +319 -0
  38. data/lib/hanko_xml/xades.rb +287 -0
  39. data/lib/hanko_xml/xml.rb +93 -0
  40. data/lib/hanko_xml/xmldsig-core-schema.xsd +332 -0
  41. data/lib/hanko_xml.rb +122 -0
  42. data/sig/hanko_xml/key.rbs +48 -0
  43. data/sig/hanko_xml/native.rbs +12 -0
  44. data/sig/hanko_xml/native_version.rbs +13 -0
  45. data/sig/hanko_xml/nokogiri_ext.rbs +23 -0
  46. data/sig/hanko_xml/placement.rbs +27 -0
  47. data/sig/hanko_xml/policy.rbs +83 -0
  48. data/sig/hanko_xml/reference.rbs +36 -0
  49. data/sig/hanko_xml/signed_document.rbs +66 -0
  50. data/sig/hanko_xml/signer.rbs +50 -0
  51. data/sig/hanko_xml/template.rbs +41 -0
  52. data/sig/hanko_xml/verification.rbs +100 -0
  53. data/sig/hanko_xml/verifier.rbs +45 -0
  54. data/sig/hanko_xml/xades.rbs +70 -0
  55. data/sig/hanko_xml.rbs +62 -0
  56. metadata +105 -0
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HankoXml
4
+ VERSION = "0.1.0"
5
+ end
@@ -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"