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.
@@ -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