dnsruby 1.73.1 → 2.0.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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -14,6 +14,10 @@
14
14
  # limitations under the License.
15
15
  # ++
16
16
 
17
+ require 'weakref'
18
+ require 'securerandom'
19
+ require 'dnsruby/serial_time'
20
+
17
21
 
18
22
  # This class does verification/validation from a single point - signed root,
19
23
  # DLV, trust anchors. Dnssec controls a set of these to perform validation for
@@ -21,52 +25,141 @@
21
25
  # This class should only be used by Dnsruby
22
26
  module Dnsruby
23
27
  class SingleVerifier # :nodoc: all
28
+ # Configured anchors are separate; only learned trust is capacity-limited.
29
+ MAX_LEARNED_KEYS = 4096
30
+ MAX_LEARNED_DS_RECORDS = 4096
31
+
32
+ # RRsets carry only serializable provenance, never a verifier, mutex or
33
+ # validation budget. The weak registry lets another verifier check whether
34
+ # the original trust source has since been removed.
35
+ LearnedTrust = Struct.new(:expiration, :signature_expiration, :transaction, :sources, :operational)
36
+ VALIDATION_ALGORITHMS = [Algorithms.RSASHA1, Algorithms.RSASHA1_NSEC3_SHA1,
37
+ Algorithms.RSASHA256, Algorithms.RSASHA512,
38
+ Algorithms.ECDSAP256SHA256, Algorithms.ECDSAP384SHA384, Algorithms.ED25519, Algorithms.ED448].freeze
39
+ # RFC 9905 retains implementation support for RSA/SHA-1 but requires
40
+ # validating resolvers to treat it as unsupported operationally. Explicit
41
+ # verify() calls keep their existing cryptographic verification behaviour.
42
+ OPERATIONAL_ALGORITHMS = (VALIDATION_ALGORITHMS -
43
+ [Algorithms.RSASHA1, Algorithms.RSASHA1_NSEC3_SHA1]).freeze
44
+ private_constant :LearnedTrust, :VALIDATION_ALGORITHMS, :OPERATIONAL_ALGORITHMS
45
+ @provenance_mutex = Mutex.new
46
+ @trust_sources = {}
47
+ @next_source_id = 0
48
+ @source_namespace = SecureRandom.hex(16).freeze
49
+
50
+ def self.register_trust_source(verifier)
51
+ @provenance_mutex.synchronize do
52
+ @trust_sources.delete_if { |_id, source| !source.weakref_alive? }
53
+ @next_source_id += 1
54
+ identity = [@source_namespace, Process.pid, @next_source_id].freeze
55
+ @trust_sources[identity] = WeakRef.new(verifier)
56
+ identity
57
+ end
58
+ end
59
+
60
+ def self.trust_source(id)
61
+ @provenance_mutex.synchronize do
62
+ source = @trust_sources[id]
63
+ source.__getobj__ if source && source.weakref_alive?
64
+ end
65
+ rescue WeakRef::RefError
66
+ nil
67
+ end
68
+ private_class_method :register_trust_source, :trust_source
69
+
24
70
  class VerifierType
25
71
  ROOT = 0
26
72
  ANCHOR = 1
27
73
  DLV = 2
28
74
  end
29
75
  def initialize(vtype)
76
+ @trust_mutex = Mutex.new
77
+ @trust_generation = 0
78
+ @trust_configuration_generation = 0
79
+ @trust_source_id = SingleVerifier.send(:register_trust_source, self)
30
80
  @verifier_type = vtype
31
81
  @added_dlv_key = false
32
- # The DNSKEY RRs for the signed root (when it exists)
33
- @root_anchors = KeyCache.new
34
-
35
82
  # The set of trust anchors.
36
83
  # If the root is unsigned, then these must be initialised with at least
37
84
  # one trusted key by the client application, if verification is to be performed.
38
85
  @trust_anchors = KeyCache.new
39
-
40
- @dlv_registries = []
86
+ # Track configured expiration independently of KeyCache pruning so an
87
+ # expired anchor invalidates learned trust and in-flight publication.
88
+ @configured_dnskey_identities = {}
41
89
 
42
90
  # The set of keys which are trusted.
43
- @trusted_keys = KeyCache.new
91
+ @trusted_keys = KeyCache.new(max_keys: MAX_LEARNED_KEYS)
92
+ @trusted_key_provenance = {}
44
93
 
45
94
  # The set of keys which have been indicated by a DS RRSet which has been
46
95
  # signed by a trusted key. Although we have not yet located these keys, we
47
96
  # have the details (tag and digest) which can identify the keys when we
48
97
  # see them. At that point, they will be added to our trusted keys.
49
- @discovered_ds_store = []
98
+ @discovered_ds_store = DSCache.new(max_records: MAX_LEARNED_DS_RECORDS)
50
99
  # The configured_ds_store is the set of DS records which have been configured
51
100
  # by the client as trust anchors. Use Dnssec#add_trust_anchor to add these
52
101
  @configured_ds_store = []
102
+ @configured_ds_expirations = {}
103
+ @configured_ds_index = DSCache.new(max_records: nil)
53
104
  end
54
105
 
55
- def set_hints(hints)
56
- @@hints = hints
106
+ @recursor_mutex = Mutex.new
107
+ @recursor_pid = Process.pid
108
+
109
+ def self.with_recursor_lock
110
+ if @recursor_pid != Process.pid
111
+ @recursor_mutex = Mutex.new
112
+ @recursor_pid = Process.pid
113
+ @recursor = nil
114
+ end
115
+ locked = false
116
+ until (locked = @recursor_mutex.try_lock)
117
+ QueryControl.current&.check!
118
+ sleep 0.01
119
+ end
120
+ QueryControl.current&.check!
121
+ yield
122
+ ensure
123
+ @recursor_mutex.unlock if locked
57
124
  end
125
+ private_class_method :with_recursor_lock
58
126
 
59
- def get_recursor
60
- if (!defined?@@recursor)
61
- if (defined?@@hints)
62
- Recursor.set_hints(@@hints, Resolver.new)
63
- @@recursor = Recursor.new()
127
+ def self.set_recursor_hints(hints)
128
+ with_recursor_lock do
129
+ @recursor_hints = if hints.is_a?(Array)
130
+ hints.map { |hint| hint.dup.freeze }.freeze
64
131
  else
65
- @@recursor = Recursor.new
132
+ hints&.dup&.freeze
66
133
  end
134
+ @recursor = nil
135
+ Recursor.reset_hints
67
136
  end
68
- @@recursor.dnssec = true
69
- return @@recursor
137
+ end
138
+
139
+ def set_hints(hints)
140
+ SingleVerifier.set_recursor_hints(hints)
141
+ end
142
+
143
+ def self.shared_recursor
144
+ with_recursor_lock do
145
+ unless @recursor
146
+ if @recursor_hints
147
+ bootstrap = Resolver.new
148
+ begin
149
+ Recursor.set_hints(@recursor_hints, bootstrap)
150
+ ensure
151
+ bootstrap.close
152
+ end
153
+ end
154
+ @recursor = Recursor.new
155
+ @recursor.dnssec = true
156
+ end
157
+ @recursor
158
+ end
159
+ end
160
+
161
+ def get_recursor
162
+ SingleVerifier.shared_recursor
70
163
  end
71
164
 
72
165
  def get_dlv_resolver # :nodoc:
@@ -91,6 +184,10 @@ module Dnsruby
91
184
  end
92
185
  end
93
186
  def get_dlv_key(ksk) # :nodoc:
187
+ with_validation { get_dlv_key_internal(ksk) }
188
+ end
189
+
190
+ private def get_dlv_key_internal(ksk)
94
191
  # Using the KSK, get the ZSK for the DLV registry
95
192
  if (!@res && (@verifier_type == VerifierType::DLV))
96
193
  @res = get_dlv_resolver
@@ -102,6 +199,8 @@ module Dnsruby
102
199
  if (!ret)
103
200
  raise ResolvError.new("Couldn't get response from Recursor")
104
201
  end
202
+ rescue RecursionLimitError
203
+ raise
105
204
  rescue ResolvError => e
106
205
  # print "ERROR - Couldn't find the DLV key\n"
107
206
  TheLog.error("Couldn't find the DLV key\n")
@@ -124,28 +223,104 @@ module Dnsruby
124
223
  end
125
224
  # Add the
126
225
  def add_trust_anchor_with_expiration(k, expiration)
127
- if (k.type == Types.DNSKEY)
128
- # k.flags = k.flags | RR::IN::DNSKEY::SEP_KEY
129
- @trust_anchors.add_key_with_expiration(k, expiration)
130
- # print "Adding trust anchor for #{k.name}\n"
131
- TheLog.info("Adding trust anchor for #{k.name}")
132
- elsif ((k.type == Types.DS) || ((k.type == Types.DLV) && (@verifier_type == VerifierType::DLV)))
133
- @configured_ds_store.push(k)
226
+ @trust_mutex.synchronize do
227
+ @trust_configuration_generation += 1
228
+ if (k.type == Types.DNSKEY)
229
+ @trust_anchors.add_key_with_expiration(k, expiration)
230
+ identity = KeyCache.key_identity(k)
231
+ @configured_dnskey_identities[identity] = [@configured_dnskey_identities[identity] || expiration, expiration].max
232
+ TheLog.info("Adding trust anchor for #{k.name}")
233
+ elsif ((k.type == Types.DS) || ((k.type == Types.DLV) && (@verifier_type == VerifierType::DLV)))
234
+ identity = ds_identity(k)
235
+ previous = @configured_ds_expirations[identity]
236
+ @configured_ds_store.push(k) unless previous
237
+ @configured_ds_expirations[identity] = [previous || expiration, expiration].max
238
+ index_configured_ds
239
+ end
134
240
  end
135
241
  end
136
242
 
137
243
  def remove_trust_anchor(t)
138
- @trust_anchors.delete(t)
244
+ @trust_mutex.synchronize do
245
+ configured = false
246
+ removed = if (t.type == Types.DNSKEY)
247
+ configured = @configured_dnskey_identities.delete(KeyCache.key_identity(t))
248
+ @trust_anchors.delete(t)
249
+ elsif t.type == Types.DS || t.type == Types.DLV
250
+ identity = ds_identity(t)
251
+ configured = @configured_ds_expirations.delete(identity)
252
+ found = @configured_ds_store.find { |record| ds_identity(record) == identity }
253
+ @configured_ds_store.delete(found) if found
254
+ index_configured_ds
255
+ found
256
+ end
257
+ # Cached keys and in-flight discoveries may depend on this anchor.
258
+ clear_learned_keys if removed || configured
259
+ removed
260
+ end
139
261
  end
140
- # Wipes the cache of trusted keys
262
+ # Remove configured anchors and any keys learned from them.
141
263
  def clear_trust_anchors
142
- @trust_anchors = KeyCache.new
264
+ @trust_mutex.synchronize do
265
+ @trust_anchors = KeyCache.new
266
+ @configured_dnskey_identities = {}
267
+ @configured_ds_store = []
268
+ @configured_ds_expirations = {}
269
+ index_configured_ds
270
+ clear_learned_keys
271
+ end
143
272
  end
144
273
 
145
274
  def trust_anchors
146
- return @trust_anchors.keys + @configured_ds_store
275
+ @trust_mutex.synchronize do
276
+ expire_trust_anchors
277
+ @trust_anchors.keys + @configured_ds_store
278
+ end
279
+ end
280
+
281
+ # Response/failure caches need a stable, inexpensive identity for the
282
+ # current trust configuration. Additions invalidate response caches without
283
+ # invalidating otherwise valid in-flight learned-trust publication.
284
+ def validation_cache_identity # :nodoc:
285
+ @trust_mutex.synchronize do
286
+ expire_trust_anchors
287
+ [@trust_source_id, @trust_generation, @trust_configuration_generation].freeze
288
+ end
147
289
  end
148
290
 
291
+ # Caller holds @trust_mutex. As with explicit removal, cached descendants
292
+ # cannot outlive a configured anchor. Other configured anchors remain
293
+ # available and can establish fresh trust without retaining stale descendants.
294
+ def expire_trust_anchors
295
+ now = Time.now.to_i
296
+ expired = @configured_dnskey_identities.any? { |_identity, expiration| expiration <= now } ||
297
+ @configured_ds_expirations.any? { |_identity, expiration| expiration <= now }
298
+ return unless expired
299
+ @configured_dnskey_identities.delete_if { |_identity, expiration| expiration <= now }
300
+ @trust_anchors.remove_expired_keys
301
+ @configured_ds_expirations.delete_if { |_identity, expiration| expiration <= now }
302
+ @configured_ds_store.select! { |record| @configured_ds_expirations.key?(ds_identity(record)) }
303
+ index_configured_ds
304
+ clear_learned_keys
305
+ end
306
+ def ds_identity(record)
307
+ [record.name.canonical.freeze, record.klass.code, record.type.code,
308
+ record.key_tag, record.algorithm.code, record.digest_type.code, record.digestbin&.dup&.freeze].freeze
309
+ end
310
+
311
+ # Caller holds @trust_mutex. Configured anchors are never capacity-evicted.
312
+ def index_configured_ds
313
+ @configured_ds_index = DSCache.new(max_records: nil)
314
+ @configured_ds_store.group_by { |record| DSCache.set_identity(record) }.each_value do |records|
315
+ rrset = RRSet.new
316
+ # Explicit anchors keep their configured objects and TTLs. Their
317
+ # deadlines are tracked separately from RRset TTL normalization.
318
+ records.each { |record| rrset.privateAdd(record) }
319
+ @configured_ds_index.add(rrset, Float::INFINITY)
320
+ end
321
+ end
322
+ private :expire_trust_anchors, :ds_identity, :index_configured_ds
323
+
149
324
  # Check that the RRSet and RRSIG record are compatible
150
325
  def check_rr_data(rrset, sigrec)#:nodoc: all
151
326
  # Each RR MUST have the same owner name as the RRSIG RR;
@@ -164,6 +339,22 @@ module Dnsruby
164
339
  raise VerifyError.new("RRSET should have same type as RRSIG for verification")
165
340
  end
166
341
 
342
+ # RFC 4035 5.3.1: a signature is authoritative only within its zone.
343
+ # DNSKEY is apex data; DS is parent-side data at a strict descendant.
344
+ zone = sigrec.signers_name
345
+ unless within_zone?(rrset.name, zone)
346
+ raise VerifyError, 'RRSet owner is outside the signing zone'
347
+ end
348
+ if rrset.type == Types.DNSKEY && rrset.name != zone
349
+ raise VerifyError, 'DNSKEY RRSet must be signed by its own zone'
350
+ end
351
+ if rrset.type == Types.DS && !rrset.name.subdomain_of?(zone)
352
+ raise VerifyError, 'DS RRSet must be signed by a parent zone'
353
+ end
354
+ if sigrec.labels < zone.labels.length
355
+ raise VerifyError, 'Wildcard source is outside the signing zone'
356
+ end
357
+
167
358
  # #Each RR in the RRset MUST have the TTL listed in the
168
359
  # #RRSIG Original TTL Field;
169
360
  # if (rrset.ttl != sigrec.original_ttl)
@@ -172,65 +363,269 @@ module Dnsruby
172
363
 
173
364
  # Now check that we are in the validity period for the RRSIG
174
365
  now = Time.now.to_i
175
- if ((sigrec.expiration < now) || (sigrec.inception > now))
366
+ unless SerialTime.valid?(sigrec.inception, sigrec.expiration, now)
176
367
  raise VerifyError.new("Signature record not in validity period")
177
368
  end
178
369
  end
179
370
 
371
+ # A bad or expired signature must not hide another usable signature.
372
+ # Filter before considering DS stores so invalid signature metadata cannot
373
+ # determine the expiration of a newly discovered key.
374
+ def valid_signatures(rrset)
375
+ error = nil
376
+ signatures = rrset.sigs.select do |sigrec|
377
+ ValidationBudget.current&.check!(1)
378
+ begin
379
+ check_rr_data(rrset, sigrec)
380
+ true
381
+ rescue VerifyError => e
382
+ error = e
383
+ false
384
+ end
385
+ end
386
+ raise error if signatures.empty? && error
387
+ signatures
388
+ end
389
+ private :valid_signatures
390
+
180
391
  # Add the specified keys to the trusted key cache.
181
392
  # k can be a KeyCache, or an RRSet of DNSKEYs.
182
- def add_trusted_key(k)
183
- @trusted_keys.add(k)
393
+ def add_trusted_key(k, expiration = nil)
394
+ provenance = learned_provenance(k)
395
+ return false if provenance && !usable_provenance?(provenance)
396
+ expiration ||= provenance&.expiration
397
+ # Only an existing version of an incoming key can prevent its replacement.
398
+ # Readers check other provenance before use; checking the whole cache here
399
+ # makes each independent key discovery proportional to every cached zone.
400
+ incoming = k.is_a?(RRSet) ? k.rrs : (k.is_a?(KeyCache) ? k.keys : [])
401
+ identities = incoming.map { |key| KeyCache.key_identity(key) }
402
+ previous = @trust_mutex.synchronize do
403
+ identities.filter_map do |identity|
404
+ trust = @trusted_key_provenance[identity]
405
+ [identity, trust] if trust
406
+ end.to_h
407
+ end
408
+ invalid = previous.reject { |_identity, trust| usable_provenance?(trust) }
409
+ publish_trust do
410
+ invalid.each do |identity, trust|
411
+ next unless @trusted_key_provenance[identity].equal?(trust)
412
+ @trusted_keys.delete_identity(identity)
413
+ @trusted_key_provenance.delete(identity)
414
+ end
415
+ if provenance && (operation = trust_operation)
416
+ k.rrs.each { |key| operation[:keys][KeyCache.key_identity(key)] = [key, provenance] }
417
+ end
418
+ result = @trusted_keys.add(k, expiration)
419
+ if provenance && expiration && expiration > Time.now.to_i
420
+ k.rrs.each do |key|
421
+ identity = KeyCache.key_identity(key)
422
+ previous = @trusted_key_provenance[identity]
423
+ if !previous || (!previous.operational && provenance.operational) ||
424
+ (previous.operational == provenance.operational && previous.expiration < expiration)
425
+ @trusted_key_provenance[identity] = provenance
426
+ end
427
+ end
428
+ end
429
+ @trusted_key_provenance.delete_if { |identity, _trust| !@trusted_keys.include_identity?(identity) }
430
+ result
431
+ end
184
432
  end
185
433
 
186
434
  def add_root_ds(ds)
187
- @configured_ds_store.push(ds)
435
+ @trust_mutex.synchronize do
436
+ @trust_configuration_generation += 1
437
+ identity = ds_identity(ds)
438
+ @configured_ds_store.push(ds) unless @configured_ds_expirations.key?(identity)
439
+ @configured_ds_expirations[identity] = Float::INFINITY
440
+ index_configured_ds
441
+ end
188
442
  end
189
443
 
190
- # Wipes the cache of trusted keys
444
+ # Clear learned trust and configured DS/DLV anchors, retaining DNSKEY anchors.
191
445
  def clear_trusted_keys
192
- @trusted_keys = KeyCache.new
446
+ @trust_mutex.synchronize do
447
+ clear_learned_keys
448
+ @configured_ds_store = []
449
+ @configured_ds_expirations = {}
450
+ index_configured_ds
451
+ end
452
+ end
453
+
454
+ # Caller holds @trust_mutex, also used by every learned-trust publication.
455
+ def clear_learned_keys
456
+ @trust_generation += 1
457
+ @trusted_keys = KeyCache.new(max_keys: MAX_LEARNED_KEYS)
458
+ @trusted_key_provenance = {}
193
459
  @res = nil
194
- @discovered_ds_store = []
195
- @configured_ds_store = []
460
+ @discovered_ds_store = DSCache.new(max_records: MAX_LEARNED_DS_RECORDS)
196
461
  end
462
+ private :clear_learned_keys
197
463
 
198
464
  def trusted_keys
199
- discovered_ds = []
200
- @discovered_ds_store.each {|rrset|
201
- rrset.rrs.each {|rr|
202
- discovered_ds.push(rr)
203
- }
204
- }
205
- return @trusted_keys.keys + @configured_ds_store + discovered_ds
465
+ keys, configured, discovered = @trust_mutex.synchronize do
466
+ expire_trust_anchors
467
+ [cached_trust_keys, @configured_ds_store.dup, @discovered_ds_store.rrsets]
468
+ end
469
+ keys.select { |key| usable_cached_key?(key) } + configured +
470
+ discovered.select { |rrset| usable_provenance?(learned_provenance(rrset)) }.flat_map(&:rrs)
471
+ end
472
+
473
+ # Public verification and discovery methods enter here explicitly. Nested
474
+ # calls retain both the operation's limits and this verifier's trust generation.
475
+ def with_validation
476
+ ValidationBudget.with { with_trust_generation { yield } }
477
+ end
478
+ private :with_validation
479
+
480
+ # Nested verification and network discovery share the generation in which
481
+ # the outer operation started. No trust lock is held during either work.
482
+ def with_trust_generation
483
+ operations = Thread.current[:dnsruby_trust_generations] ||= {}
484
+ return yield if operations.key?(self)
485
+ generation = @trust_mutex.synchronize do
486
+ expire_trust_anchors
487
+ @trust_generation
488
+ end
489
+ operations[self] = { generation: generation, keys: {}, ds: [], rrsets: {}.compare_by_identity }
490
+ begin
491
+ yield
492
+ ensure
493
+ operations.delete(self)
494
+ Thread.current[:dnsruby_trust_generations] = nil if operations.empty?
495
+ end
496
+ end
497
+
498
+ def publish_trust
499
+ generation = trust_operation&.fetch(:generation)
500
+ @trust_mutex.synchronize do
501
+ expire_trust_anchors
502
+ ValidationBudget.current&.check!
503
+ return false if generation && generation != @trust_generation
504
+ yield
505
+ end
506
+ end
507
+ private :with_trust_generation, :publish_trust
508
+
509
+ def trust_operation
510
+ Thread.current[:dnsruby_trust_generations]&.fetch(self, nil)
511
+ end
512
+
513
+ def learned_provenance(rrset)
514
+ return unless rrset.respond_to?(:type)
515
+ trust_operation&.fetch(:rrsets)&.fetch(rrset, nil) || rrset.instance_variable_get(:@dnsruby_learned_trust)
516
+ end
517
+
518
+ def current_trust_generation?(generation)
519
+ @trust_mutex.synchronize do
520
+ expire_trust_anchors
521
+ generation == @trust_generation
522
+ end
523
+ end
524
+
525
+ def usable_provenance?(provenance)
526
+ return false unless provenance
527
+ return false if trust_operation&.fetch(:operational, false) && !provenance.operational
528
+ now = Time.now.to_i
529
+ return false if provenance.signature_expiration < now
530
+ return false unless provenance.expiration > now ||
531
+ provenance.transaction.equal?(ValidationBudget.current&.transaction)
532
+ provenance.sources.all? do |identity, generation|
533
+ source = SingleVerifier.send(:trust_source, identity)
534
+ source && source.send(:current_trust_generation?, generation)
535
+ end
536
+ end
537
+
538
+ def key_provenance(key)
539
+ identity = KeyCache.key_identity(key)
540
+ configured = @trust_mutex.synchronize do
541
+ expire_trust_anchors
542
+ @configured_dnskey_identities.key?(identity)
543
+ end
544
+ return if configured
545
+ local = trust_operation&.fetch(:keys)&.fetch(identity, nil)
546
+ return local.last if local
547
+ @trust_mutex.synchronize { @trusted_key_provenance[identity] }
548
+ end
549
+
550
+ def usable_cached_key?(key)
551
+ provenance = key_provenance(key)
552
+ !provenance || usable_provenance?(provenance)
553
+ end
554
+
555
+ # Caller holds @trust_mutex. Keep provenance bounded by the live key cache.
556
+ def cached_trust_keys(signatures = nil)
557
+ keys = signatures ? @trusted_keys.matching_keys(signatures) : @trusted_keys.keys
558
+ unless signatures
559
+ @trusted_key_provenance.delete_if { |identity, _trust| !@trusted_keys.include_identity?(identity) }
560
+ end
561
+ keys
562
+ end
563
+
564
+ def admit_provenance(provenance)
565
+ return unless provenance
566
+ raise VerifyError, 'Learned trust is no longer valid' unless usable_provenance?(provenance)
567
+ LearnedTrust.new(provenance.expiration, provenance.signature_expiration,
568
+ ValidationBudget.current.transaction, provenance.sources, provenance.operational).freeze
569
+ end
570
+
571
+ def admit_learned_rrset(rrset)
572
+ provenance = learned_provenance(rrset)
573
+ return unless provenance
574
+ admitted = admit_provenance(provenance)
575
+ trust_operation[:rrsets][rrset] = admitted
576
+ admitted
206
577
  end
207
578
 
579
+ def authenticated_provenance(rrset, signature, expiration, parent = nil)
580
+ # Retain the original receipt deadline when the same response is checked
581
+ # again. Cache lifetime and permission to finish this request are distinct.
582
+ previous = learned_provenance(rrset)
583
+ expiration = [expiration, previous.expiration].min if previous
584
+ signature_expiration = SerialTime.absolute(signature.expiration, Time.now.to_i)
585
+ raise VerifyError, 'Signature expiration is ambiguous' unless signature_expiration
586
+ if parent
587
+ expiration = [expiration, parent.expiration].min
588
+ signature_expiration = [signature_expiration, parent.signature_expiration].min
589
+ end
590
+ sources = (parent ? parent.sources : []) + [[@trust_source_id, trust_operation.fetch(:generation)].freeze]
591
+ LearnedTrust.new(expiration, signature_expiration, ValidationBudget.current.transaction,
592
+ sources.uniq.freeze, algorithm_supported?(signature.algorithm, operational: true) &&
593
+ (!parent || parent.operational)).freeze
594
+ end
595
+
596
+ def retain_learned_rrset(rrset, provenance)
597
+ # The frozen value is safe to retain across verifier fallback, cloning,
598
+ # cache eviction, and Marshal round trips.
599
+ rrset.instance_variable_set(:@dnsruby_learned_trust, provenance)
600
+ rrset.rrs.each { |rr| rr.instance_variable_set(:@dnsruby_learned_trust, provenance) }
601
+ trust_operation[:rrsets][rrset] = provenance if trust_operation
602
+ end
603
+ private :trust_operation, :learned_provenance, :current_trust_generation?,
604
+ :usable_provenance?, :key_provenance, :usable_cached_key?,
605
+ :authenticated_provenance, :retain_learned_rrset, :cached_trust_keys,
606
+ :admit_provenance, :admit_learned_rrset
607
+
208
608
  # Check that the key fits a signed DS record key details
209
- # If so, then add the key to the trusted keys
210
- def check_ds(key, ds_rrset)#:nodoc: all
211
- expiration = 0
212
- found = false
213
- ds_rrset.sigs.each { |sig|
214
- if ((sig.type_covered == Types.DS) || ((sig.type_covered == Types.DLV)&& (@verifier_type==VerifierType::DLV)))
215
- if (sig.inception <= Time.now.to_i)
216
- # Check sig.expiration, sig.algorithm
217
- if (sig.expiration > expiration)
218
- expiration = sig.expiration
219
- end
220
- end
221
- end
222
- }
223
- if (expiration > 0)
224
- ds_rrset.rrs.each { |ds|
225
- if ((ds.type === Types.DS) || ((ds.type == Types.DLV) && (@verifier_type == VerifierType::DLV)))
226
- if (ds.check_key(key))
227
- @trusted_keys.add_key_with_expiration(key, expiration)
228
- found = true
229
- end
230
- end
231
- }
609
+ # A DS digest match identifies a candidate; only a verified DNSKEY RRset
610
+ # can publish that key and establish its authenticated cache lifetime.
611
+ def check_ds(key, ds_rrset, expiration = nil)#:nodoc: all
612
+ with_validation { check_ds_internal(key, ds_rrset, expiration) }
613
+ end
614
+
615
+ private def check_ds_internal(key, ds_rrset, expiration)
616
+ unless expiration
617
+ verification = {}
618
+ verify_rrset(ds_rrset, nil, verification)
619
+ expiration = verification.fetch(:expiration)
620
+ end
621
+ provenance = learned_provenance(ds_rrset)
622
+ return false if provenance && !usable_provenance?(provenance)
623
+ return false if !provenance && expiration < Time.now.to_i
624
+ preferred_ds_records(ds_rrset.rrs).any? do |ds|
625
+ ValidationBudget.current&.check!(1)
626
+ next false unless ds.check_key(key)
627
+ true
232
628
  end
233
- return found
234
629
  end
235
630
 
236
631
  # Verify the specified message (or RRSet) using the set of trusted keys.
@@ -247,6 +642,17 @@ module Dnsruby
247
642
  #
248
643
  # Returns true if the message verifies OK, and false otherwise.
249
644
  def verify(msg, keys = nil)
645
+ verify_result(msg, keys) == :secure
646
+ end
647
+
648
+ def verify_result(msg, keys = nil) # :nodoc:
649
+ with_validation do
650
+ result = verify_internal(msg, keys)
651
+ result == true ? :secure : result
652
+ end
653
+ end
654
+
655
+ private def verify_internal(msg, keys)
250
656
  if (msg.kind_of?RRSet)
251
657
  if (msg.type == Types.DNSKEY)
252
658
  return verify_key_rrset(msg, keys)
@@ -262,40 +668,74 @@ module Dnsruby
262
668
  # new total set of keys to check the rest of the rrsets.
263
669
  # Return true if we can verify the whole message.
264
670
 
265
- msg.each_section do |section|
671
+ discard_unsupported_nsec3_proofs(msg)
672
+ sections = msg.section_rrsets
673
+ context = { message: msg, proofs: {}, sections: sections,
674
+ verified: {}.compare_by_identity, discarded_optional: {}.compare_by_identity }
675
+ insecure = trust_operation&.fetch(:insecure_rrsets, {}) || {}
676
+ insecure_set = ->(set) { insecure[[set.name.canonical, set.klass.code, set.type.code]] }
677
+ sections.each do |section, rrsets|
266
678
  # print "Checking section : #{section}\n"
267
- ds_rrsets = section.rrsets(Types.DS)
679
+ ds_rrsets = rrsets.select { |rrset| rrset.type == Types.DS }
268
680
  if ((!ds_rrsets || ds_rrsets.length == 0) && (@verifier_type == VerifierType::DLV))
269
- ds_rrsets = section.rrsets(Types.DLV)
681
+ ds_rrsets = rrsets.select { |rrset| rrset.type == Types.DLV }
270
682
  end
271
683
  ds_rrsets.each {|ds_rrset|
272
- if ((ds_rrset && ds_rrset.rrs.length > 0) && !verify_ds_rrset(ds_rrset, keys, msg))
273
- raise VerifyError.new("Failed to verify DS RRSet")
274
- # return false
684
+ next if insecure_set.call(ds_rrset)
685
+ if section == 'additional'
686
+ verify_optional_rrset(ds_rrset, context) do |optional_context|
687
+ verify_ds_rrset(ds_rrset, keys, msg, optional_context)
688
+ end
689
+ elsif ds_rrset.rrs.any? && !verify_ds_rrset(ds_rrset, keys, msg, context)
690
+ raise VerifyError, 'Failed to verify DS RRSet'
275
691
  end
276
692
  }
277
693
 
278
- key_rrsets = section.rrsets(Types.DNSKEY)
694
+ key_rrsets = rrsets.select { |rrset| rrset.type == Types.DNSKEY }
279
695
  key_rrsets.each {|key_rrset|
280
- if ((key_rrset && key_rrset.rrs.length > 0) && !verify_key_rrset(key_rrset, keys))
281
- raise VerifyError.new("Failed to verify DNSKEY RRSet")
282
- # return false
696
+ next if insecure_set.call(key_rrset)
697
+ if section == 'additional'
698
+ verify_optional_rrset(key_rrset, context) do |optional_context|
699
+ verify_key_rrset(key_rrset, keys, optional_context)
700
+ end
701
+ elsif key_rrset.rrs.any? && !verify_key_rrset(key_rrset, keys, context)
702
+ raise VerifyError, 'Failed to verify DNSKEY RRSet'
283
703
  end
284
704
  }
285
705
  end
286
706
 
287
- verify_nsecs(msg)
288
-
289
707
  # Then, look through all the remaining RRSets, and verify them all (unless not necessary).
290
- msg.section_rrsets.each do |section, rrsets|
708
+ sections.each do |section, rrsets|
291
709
  rrsets.each do |rrset|
710
+ next if context[:discarded_optional].key?(rrset)
711
+ if section != 'additional' && insecure_set.call(rrset)
712
+ context[:insecure] = true
713
+ next
714
+ end
715
+ # Key learning already authenticated these exact per-message sets.
716
+ next if context[:verified].key?(rrset)
717
+ if section == 'answer' && rrset.type == Types.CNAME && rrset.sigs.empty?
718
+ next if verify_synthesized_cname(rrset, keys, context)
719
+ end
292
720
  # If delegation NS or glue AAAA/A, then don't expect RRSIG.
293
721
  # Otherwise, expect RRSIG and fail verification if RRSIG is not present
294
722
 
295
723
  if ((section == "authority") && (rrset.type == Types.NS))
724
+ # Parent-side delegation NS records are unsigned, including when
725
+ # the query is below the cut and its DS RRset authenticated above.
726
+ question = msg.question.first
727
+ terminal, answered = terminal_answer(msg, question) if question
728
+ if question && !answered && rrset.klass == question.qclass &&
729
+ within_zone?(terminal, rrset.name) && rrset.sigs.empty? &&
730
+ rrsets.any? { |set| set.type == Types.DS && set.name == rrset.name &&
731
+ set.klass == rrset.klass && context[:verified].key?(set) &&
732
+ rrset.name.subdomain_of?(context[:verified][set][:signature].signers_name) }
733
+ next
734
+ end
296
735
  # Check for delegation
297
- dsrrset = msg.authority.rrsets('DS')[0]
298
- if ((msg.answer.size == 0) && (!dsrrset) && (rrset.type == Types.NS)) # (isDelegation)
736
+ dsrrset = rrsets.find { |set| set.type == Types.DS && set.name == rrset.name && set.klass == rrset.klass }
737
+ if question && !answered && rrset.klass == question.qclass &&
738
+ within_zone?(terminal, rrset.name) && !dsrrset
299
739
  # Now check NSEC(3) records for absence of DS and SOA
300
740
  nsec = msg.authority.rrsets('NSEC')[0]
301
741
  if (!nsec || (nsec.length == 0))
@@ -326,286 +766,420 @@ module Dnsruby
326
766
  end
327
767
  end
328
768
 
329
- if (section == "additional")
330
- # check for glue
331
- # if the ownername (in the addtional section) of the glue address is the same or longer as the ownername of the NS record, it is glue
332
- if (msg.additional.size > 0)
333
- arec = msg.additional.rrsets('A')[0]
334
- if (!arec || arec.rrs.length == 0)
335
- arec = msg.additional.rrsets('AAAA')[0]
769
+ if section == 'additional'
770
+ # Unsigned glue is needed for iterative discovery. Signed optional
771
+ # data is useful only when it verifies without another lookup; a
772
+ # failure must not reject an independently authenticated answer.
773
+ if rrset.sigs.any?
774
+ verify_optional_rrset(rrset, context) do |optional_context|
775
+ verify_rrset(rrset, keys, optional_context)
336
776
  end
337
- ns_rrsets = msg.additional.rrsets('NS')
338
- ns_rrsets.each {|ns_rrset|
339
- if (ns_rrset.length > 0)
340
- nsname = ns_rrset.rrs()[0].name
341
- if (arec && arec.rrs().length > 0)
342
- aname = arec.rrs()[0].name
343
- if (nsname.subdomain_of?aname)
344
- next
345
- end
346
- end
347
- end
348
- }
349
777
  end
350
- end
351
- # If records are in additional, and no RRSIG, that's Ok - just don't use them!
352
- if ((section == "additional") && (rrset.sigs.length == 0))
353
- # @TODO@ Make sure that we don't cache these records!
354
778
  next
355
779
  end
356
780
  # else verify RRSet
357
781
  # print "About to verify #{rrset.name}, #{rrset.type}\n"
358
- if (!verify_rrset(rrset, keys))
782
+ if (!verify_rrset(rrset, keys, context))
359
783
  # print "FAILED TO VERIFY RRSET #{rrset.name}, #{rrset.type}\n"
360
784
  TheLog.debug("Failed to verify rrset")
361
785
  return false
362
786
  end
363
787
  end
364
788
  end
365
- return true
789
+ verify_nsecs(msg, context)
790
+ discard_optional_rrsets(msg, context)
791
+ apply_verified_lifetimes(msg, context)
792
+ return context[:insecure] ? :insecure : true
366
793
  end
367
794
 
368
- def verify_nsecs(msg) # :nodoc:
369
- # NSEC(3) handling. Get NSEC(3)s in four cases : (RFC 4035, section 3.1.3)
370
- # a) No data - <SNAME, SCLASS> matches, but no <SNAME, SCLASS, STYPE) (§3.1.3.1)
371
- # - will expect NSEC in Authority (and associated RRSIG)
372
- # - NOERROR returned
373
- # b) Name error - no RRSets that match <SNAME, SCLASS> either exactly or through wildcard expansion (§3.1.3.2)
374
- # - NSEC wil prove i) no exact match for <SNAME, SCLASS>, and ii) no RRSets that could match through wildcard expansion
375
- # - this may be proved in one or more NSECs (and associated RRSIGs)
376
- # - NXDOMAIN returned - should ensure we verify!
377
- # c) Wildcard answer - No <SNAME, SCLASS> direct matches, but matches <SNAME, SCLASS, STYPE> through wildcard expansion (§3.1.3.3)
378
- # - Answer section must include wildcard-expanded answer (and associated RRSIGs)
379
- # - label count in answer RRSIG indicates wildcard RRSet was expanded (less labels than in owner name)
380
- # - Authority section must include NSEC (and RRSIGs) proving that zone does not contain a closer match
381
- # - NOERROR returned
382
- # d) Wildcard no data - No <SNAME, SCLASS> direct. <SNAME, SCLASS> yes but <SNAME, SCLASS, STYPE> no through wildcard expansion (§3.1.3.4)
383
- # - Authority section contains NSECs (and RRSIGs) for :
384
- # i) NSEC proving no RRSets matching STYPE at wildcard owner name that matched <SNAME, SCLASS> via wildcard expansion
385
- # ii) NSEC proving no RRSets in zone that would have been closer match for <SNAME, SCLASS>
386
- # - this may be proved by one or more NSECs (and associated RRSIGs)
387
- # - NOERROR returned
388
- #
389
- # Otherwise no NSECs should be returned.
390
-
391
- # So, check for NSEC records in response, and work out what type of answer we have.
392
- # Then, if NSECs are present, make sure that we prove what they said they would.
393
- # What if the message *should* have no NSEC records? That can only be known by the validator.
394
- # We will assume that the validator has checked the (non)-existence of NSEC records - we should not
395
- # get upset if there aren't any. However, if there are, then we should verify that they say the right thing
396
- qtype = msg.question()[0].qtype
397
- return if (msg.rcode == RCode.NOERROR && ((qtype == Types.ANY) || (qtype == Types.NSEC) || (qtype == Types.NSEC3)))
398
- if ((msg.rrsets('NSEC').length > 0) || (msg.rrsets('NSEC3').length > 0))
399
- if (msg.rcode == RCode.NXDOMAIN)
400
- # print "Checking NSECs for Name Error\n"
401
- # Name error - NSEC wil prove i) no exact match for <SNAME, SCLASS>, and ii) no RRSets that could match through wildcard expansion
402
- # - this may be proved in one or more NSECs (and associated RRSIGs)
403
- check_name_in_nsecs(msg)
404
- return check_no_wildcard_expansion(msg)
405
- elsif (msg.rcode == RCode.NOERROR)
406
- if (msg.answer.length > 0)
407
- # print "Checking NSECs for wildcard expansion\n"
408
- # wildcard expansion answer - check NSECs!
409
- # We want to make sure that the NSEC tells us that there is no closer match for this name
410
- # @TODO@ We need to make replace the RRSIG name with the wildcard name before we can verify it correctly.
411
- check_num_rrsig_labels(msg)
412
- return check_name_in_nsecs(msg, msg.question()[0].qtype, true)
413
- else
414
- # Either no data or wildcard no data - check to see which
415
- # Should be able to tell this by checking the number of labels in the NSEC records.
416
- # Sort these two last cases out!
417
- isWildcardNoData = false
418
- [msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
419
- nsec_rrsets.each {|nsec_rrset|
420
- nsec_rrset.rrs.each {|nsec|
421
- # print "Checking nsec to see if wildcard : #{nsec}\n"
422
- if (nsec.name.wild? ||(nsec.name.labels.length < msg.question()[0].qname.labels.length))
423
- isWildcardNoData = true
424
- end
425
- }
426
- }
427
- }
795
+ # Unsupported NSEC3 parameters do not invalidate another usable proof.
796
+ # Keep a mixed RRset intact for signature verification; usable_hash? filters
797
+ # its unsupported records when the proof is evaluated. Explicit NSEC3
798
+ # answers remain available to callers inspecting these records.
799
+ def discard_unsupported_nsec3_proofs(message)
800
+ supported = {}
801
+ message.authority.each do |record|
802
+ ValidationBudget.current&.check!(1)
803
+ next unless record.type == Types.NSEC3
804
+ identity = [record.name.canonical, record.klass.code]
805
+ supported[identity] = !!supported[identity] || record.supported_parameters?
806
+ end
807
+ return unless supported.value?(false)
808
+ message.authority.delete_if do |record|
809
+ ValidationBudget.current&.check!(1)
810
+ type = record.type == Types.RRSIG ? record.type_covered : record.type
811
+ type == Types.NSEC3 && supported[[record.name.canonical, record.klass.code]] == false
812
+ end
813
+ message.update_counts
814
+ end
815
+ private :discard_unsupported_nsec3_proofs
816
+
817
+ # Optional verification may contribute authenticated data, but never the
818
+ # security classification of the required answer. Drop failed signed sets
819
+ # from the returned message so response caching cannot retain them as part
820
+ # of a successfully validated response. Resource limits still abort all work.
821
+ def verify_optional_rrset(rrset, context)
822
+ optional_context = context.merge(insecure: false)
823
+ return true if yield(optional_context) && !optional_context[:insecure]
824
+ discard_optional_rrset(rrset, context)
825
+ false
826
+ rescue VerifyError => error
827
+ raise if error.is_a?(ValidationLimitError)
828
+ discard_optional_rrset(rrset, context)
829
+ false
830
+ end
428
831
 
429
- if (isWildcardNoData)
430
- # print "Checking NSECs for wildcard no data\n"
431
- # Check NSECs -
432
- # i) NSEC proving no RRSets matching STYPE at wildcard owner name that matched <SNAME, SCLASS> via wildcard expansion
433
- check_name_not_in_wildcard_nsecs(msg)
434
- # ii) NSEC proving no RRSets in zone that would have been closer match for <SNAME, SCLASS>
435
- return check_name_in_and_type_not_in_nsecs(msg)
436
- else # (isNoData)
437
- # print "Checking NSECs for No data\n"
438
- # Check NSEC types covered to make sure this type not present.
439
- return check_name_in_and_type_not_in_nsecs(msg)
440
- end
441
- end
832
+ def discard_optional_rrset(rrset, context)
833
+ context[:discarded_optional][rrset] = true
834
+ context[:verified].delete(rrset)
835
+ end
836
+
837
+ def discard_optional_rrsets(message, context)
838
+ return if context[:discarded_optional].empty?
839
+ discarded = context[:discarded_optional].each_key.each_with_object({}) do |rrset, index|
840
+ ValidationBudget.current&.check!(1)
841
+ type = rrset.type == Types.RRSIG ? rrset.sigs.first.type_covered : rrset.type
842
+ index[[rrset.name.canonical, rrset.klass.code, type.code]] = true
843
+ end
844
+ message.additional.delete_if do |record|
845
+ ValidationBudget.current&.check!(1)
846
+ type = record.type == Types.RRSIG ? record.type_covered : record.type
847
+ discarded[[record.name.canonical, record.klass.code, type.code]]
848
+ end
849
+ message.update_counts
850
+ end
851
+ private :verify_optional_rrset, :discard_optional_rrset, :discard_optional_rrsets
852
+
853
+ # RFC 6672 5.3.1: a synthesized CNAME inherits authentication from the
854
+ # DNAME, provided that the complete label substitution is identical. Only
855
+ # answer-section DNAMEs can authenticate this unsigned answer CNAME.
856
+ def verify_synthesized_cname(cname, keys, context)
857
+ dname = context[:sections].fetch('answer').select do |set|
858
+ set.type == Types.DNAME && set.klass == cname.klass && cname.name.subdomain_of?(set.name)
859
+ end.max_by { |set| set.name.labels.length }
860
+ return false unless dname
861
+ unless cname.rrs.length == 1 && dname.rrs.length == 1 &&
862
+ cname.rrs.first.domainname == dname_substitution(cname.name, dname.rrs.first)
863
+ raise VerifyError, 'CNAME does not match its DNAME substitution'
864
+ end
865
+ unless context[:verified].key?(dname)
866
+ return false unless verify_rrset(dname, keys, context)
867
+ end
868
+ expiration = context[:verified].fetch(dname).fetch(:expiration)
869
+ received_at = cname.rrs.first.instance_variable_get(:@dnsruby_received_at) || Time.now.to_i
870
+ previous = cname.rrs.first.instance_variable_get(:@dnsruby_authenticated_expiration)
871
+ expiration = [expiration, received_at + cname.ttl, previous].compact.min
872
+ cname.ttl = [cname.ttl, dname.ttl, [expiration - Time.now.to_i, 0].max].min
873
+ context[:verified][cname] = { signature: nil, expiration: expiration }
874
+ true
875
+ end
876
+
877
+ def dname_substitution(name, dname)
878
+ Name.new(name.labels.first(name.labels.length - dname.name.labels.length) +
879
+ dname.domainname.labels, true)
880
+ rescue ResolvError => e
881
+ raise VerifyError, "Invalid DNAME substitution: #{e.message}"
882
+ end
883
+ private :verify_synthesized_cname, :dname_substitution
884
+
885
+ # Check denial at the end of an alias chain, not at the original QNAME.
886
+ # A CNAME-only answer without a negative SOA can legitimately be partial.
887
+ # Positive wildcard proofs are checked when their signatures verify.
888
+ def verify_nsecs(msg, context = nil) # :nodoc:
889
+ question = msg.question.first
890
+ return unless question && [RCode.NOERROR, RCode.NXDOMAIN].include?(msg.rcode)
891
+ name, answered = terminal_answer(msg, question)
892
+ if trust_operation&.fetch(:insecure_names, {})&.key?([name.canonical, question.qclass.code])
893
+ context[:insecure] = true if context
894
+ return
895
+ end
896
+ return if msg.rcode == RCode.NOERROR && answered
897
+ raise VerifyError, 'NXDOMAIN response contains the requested answer' if answered
898
+
899
+ authority = context ? context[:sections].fetch('authority') : msg.authority.rrsets
900
+ klass, qtype = question.qclass, question.qtype
901
+ soa = authority.select do |set|
902
+ set.type == Types.SOA && set.klass == klass && within_zone?(name, set.name)
903
+ end.max_by { |set| set.name.labels.length }
904
+ referral = if msg.rcode == RCode.NOERROR && !soa
905
+ authority.select do |set|
906
+ set.type == Types.NS && set.klass == klass && within_zone?(name, set.name)
907
+ end.max_by { |set| set.name.labels.length }
908
+ end
909
+ if referral
910
+ # A signed delegation with DS needs no denial. An unsigned referral
911
+ # proves the absence of DS at the delegation, not absence of QTYPE.
912
+ return if authority.any? { |set| set.type == Types.DS && set.name == referral.name && set.klass == klass }
913
+ name, qtype = referral.name, Types.DS
914
+ elsif msg.rcode == RCode.NOERROR && name != question.qname && !soa
915
+ return
916
+ end
917
+
918
+ proofs = authority.select { |set| [Types.NSEC, Types.NSEC3].include?(set.type) && set.klass == klass }
919
+ if proofs.empty?
920
+ raise VerifyError, "Missing denial proof for #{name}"
921
+ end
922
+ zone = soa&.name
923
+ if context && soa
924
+ signature = context[:verified][soa]&.fetch(:signature)
925
+ raise VerifyError, 'Negative SOA is not signed by its zone' unless signature && signature.signers_name == zone
926
+ end
927
+ groups = {}
928
+ proofs.each do |set|
929
+ ValidationBudget.current&.check!(1)
930
+ signature = context && context[:verified][set]&.fetch(:signature)
931
+ next if context && !signature
932
+ signing_zone = signature&.signers_name || (set.type == Types.NSEC3 ? set.name.strip_label : zone)
933
+ next unless signing_zone && within_zone?(name, signing_zone)
934
+ next if zone && signing_zone != zone
935
+ # DS denial at a non-root delegation must come from the parent.
936
+ next if qtype == Types.DS && name == signing_zone && !name.labels.empty?
937
+ next if signature && signature.labels != set.name.labels.length &&
938
+ !(set.name.wild? && signature.labels == set.name.labels.length - 1)
939
+ (groups[[signing_zone, set.type]] ||= []).concat(set.rrs)
940
+ end
941
+ insecure = false
942
+ groups.each do |(signing_zone, type), records|
943
+ result = if type == Types.NSEC3
944
+ nsec3_denial(records, name, qtype, signing_zone, msg.rcode, !!referral)
442
945
  else
443
- # Anything we should do here?
946
+ nsec_denial(records, name, qtype, signing_zone, msg.rcode, !!referral)
947
+ end
948
+ if result == :secure
949
+ return true
950
+ elsif result == :insecure
951
+ insecure = true
952
+ end
953
+ end
954
+ if insecure
955
+ context[:insecure] = true if context
956
+ if context && (expirations = trust_operation&.fetch(:insecurity_expirations, nil))
957
+ expirations << context[:verified].values.map { |verified| verified[:expiration] }.min
444
958
  end
959
+ return true
445
960
  end
961
+ raise VerifyError, "No authenticated denial proof for #{name}, #{qtype}"
962
+ end
963
+
964
+ def terminal_answer(msg, question)
965
+ name = question.qname
966
+ seen = {}
967
+ loop do
968
+ ValidationBudget.current&.check!(1)
969
+ raise VerifyError, 'CNAME loop in answer' if seen[name.canonical]
970
+ seen[name.canonical] = true
971
+ records = msg.answer.select { |rr| rr.name == name && rr.klass == question.qclass && rr.type != Types.RRSIG }
972
+ return [name, true] if records.any? { |rr| question.qtype == Types.ANY || rr.type == question.qtype }
973
+ aliases = records.select { |rr| rr.type == Types.CNAME }
974
+ if aliases.empty?
975
+ # A DNSSEC-aware server may omit the synthesized CNAME (RFC 6672
976
+ # 5.3.1); the authenticated DNAME still redirects the question.
977
+ dnames = msg.answer.select do |rr|
978
+ rr.type == Types.DNAME && rr.klass == question.qclass && name.subdomain_of?(rr.name)
979
+ end
980
+ return [name, false] if dnames.empty?
981
+ closest = dnames.max_by { |rr| rr.name.labels.length }
982
+ if dnames.any? { |rr| rr.name == closest.name && rr.domainname != closest.domainname }
983
+ raise VerifyError, 'Conflicting DNAME targets in answer'
984
+ end
985
+ name = dname_substitution(name, closest)
986
+ next
987
+ end
988
+ targets = aliases.map(&:domainname).uniq
989
+ raise VerifyError, 'Conflicting CNAME targets in answer' unless targets.length == 1
990
+ name = targets.first
991
+ end
992
+ end
993
+
994
+ def within_zone?(name, zone)
995
+ name == zone || name.subdomain_of?(zone)
996
+ end
446
997
 
998
+ def denial_bitmap?(record, qtype, referral = false)
999
+ types = record.types
1000
+ # ANY has no data-type bit. NSEC3 can represent an empty nonterminal;
1001
+ # an exact NSEC owner necessarily has at least its own NSEC RRset.
1002
+ return record.type == Types.NSEC3 && types.empty? if qtype == Types.ANY
1003
+ # A validated NSEC and its covering RRSIG exist regardless of these
1004
+ # bitmap bits (RFC 4035 section 5.4).
1005
+ return false if record.type == Types.NSEC && [Types.NSEC, Types.RRSIG].include?(qtype)
1006
+ return false if types.include?(qtype) || types.include?(Types.CNAME)
1007
+ return types.include?(Types.NS) && !types.include?(Types.SOA) if referral
1008
+ # A parent delegation cannot deny ordinary data in the child zone.
1009
+ qtype == Types.DS || !types.include?(Types.NS) || types.include?(Types.SOA)
447
1010
  end
448
1011
 
449
- def check_num_rrsig_labels(msg) # :nodoc:
450
- # Check that the number of labels in the RRSIG is less than the number
451
- # of labels in the answer name
452
- answer_rrset = msg.answer.rrset(msg.question()[0].qname, msg.question()[0].qtype)
453
- if (answer_rrset.length == 0)
454
- raise VerifyError.new("Expected wildcard expanded answer for #{msg.question()[0].qname}")
1012
+ def nsec3_denial(records, name, qtype, zone, rcode, referral)
1013
+ records = records.select do |rr|
1014
+ ValidationBudget.current&.check!(1)
1015
+ rr.name.strip_label == zone && rr.usable_hash?
1016
+ end
1017
+ return unless records.any?
1018
+ # Do not combine independently salted/iterated chains into one proof.
1019
+ parameters = records.map { |rr| [rr.hash_alg, rr.iterations, rr.salt] }.uniq
1020
+ return unless parameters.length == 1
1021
+ hashes = {}
1022
+ hash_for = lambda do |candidate|
1023
+ hashes[candidate.canonical] ||= records.first.hash_name(candidate)
1024
+ end
1025
+ matching = lambda do |candidate|
1026
+ hashed = hash_for.call(candidate)
1027
+ find_denial_record(records) { |rr| rr.owner_hash == hashed }
1028
+ end
1029
+ covering = lambda do |candidate|
1030
+ hashed = hash_for.call(candidate)
1031
+ find_denial_record(records) { |rr| rr.covers_hash?(hashed) }
1032
+ end
1033
+ exact = matching.call(name)
1034
+ if rcode == RCode.NOERROR && exact
1035
+ return referral ? :insecure : :secure if denial_bitmap?(exact, qtype, referral)
1036
+ return
1037
+ end
1038
+ return if exact
1039
+
1040
+ child, closest, next_cover = name, name.strip_label, nil
1041
+ while within_zone?(closest, zone)
1042
+ ValidationBudget.current&.check!(1)
1043
+ if (record = matching.call(closest))
1044
+ # RFC 5155 8.3: a DNAME or parent-side delegation is not an
1045
+ # authoritative closest encloser for names below it.
1046
+ return if record.types.include?(Types.DNAME) ||
1047
+ (record.types.include?(Types.NS) && !record.types.include?(Types.SOA))
1048
+ next_cover = covering.call(child)
1049
+ break
1050
+ end
1051
+ break if closest == zone
1052
+ child, closest = closest, closest.strip_label
455
1053
  end
456
- rrsig = answer_rrset.sigs()[0]
457
- if (rrsig.labels >= msg.question()[0].qname.labels.length)
458
- raise VerifyError.new("RRSIG does not prove wildcard expansion for #{msg.question()[0].qname}")
1054
+ return unless next_cover
1055
+ if rcode == RCode.NOERROR && qtype == Types.DS
1056
+ return :insecure if next_cover.opt_out?
1057
+ return
1058
+ end
1059
+ wildcard = Name.new([Name::Label.new('*')] + closest.labels, true)
1060
+ if rcode == RCode.NXDOMAIN
1061
+ return unless covering.call(wildcard)
1062
+ else
1063
+ wildcard_record = matching.call(wildcard)
1064
+ return unless wildcard_record && denial_bitmap?(wildcard_record, qtype)
459
1065
  end
1066
+ next_cover.opt_out? ? :insecure : :secure
460
1067
  end
461
1068
 
462
- def check_no_wildcard_expansion(msg) # :nodoc:
463
- # @TODO@ Do this for NSEC3 records!!!
464
- proven_no_wildcards = false
465
- [msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
466
- nsec_rrsets.each {|nsecs|
467
- nsecs.rrs.each {|nsec|
468
- # print "Checking NSEC : #{nsec}\n"
469
- next if (nsec.name.wild?)
470
- if (check_record_proves_no_wildcard(msg, nsec))
471
- proven_no_wildcards = true
472
- end
473
- }
474
- }
475
- }
476
- if (!proven_no_wildcards)
477
- # print "No proof that no RRSets could match through wildcard expansion\n"
478
- raise VerifyError.new("No proof that no RRSets could match through wildcard expansion")
1069
+ def nsec_denial(records, name, qtype, zone, rcode, referral)
1070
+ records = records.select do |rr|
1071
+ ValidationBudget.current&.check!(1)
1072
+ within_zone?(rr.name, zone) && within_zone?(rr.next_domain, zone)
479
1073
  end
1074
+ return unless records.any?
1075
+ return unless consistent_nsec_intervals?(records)
1076
+ exact = find_denial_record(records) { |rr| rr.name == name }
1077
+ if rcode == RCode.NOERROR && exact
1078
+ return referral ? :insecure : :secure if denial_bitmap?(exact, qtype, referral)
1079
+ return
1080
+ end
1081
+ return if exact || referral
1082
+ if rcode == RCode.NOERROR && find_denial_record(records) { |rr| rr.next_domain.subdomain_of?(name) && interval_covers?(rr.name, rr.next_domain, name) }
1083
+ # An NSEC ending at a descendant proves an empty nonterminal exists.
1084
+ return :secure
1085
+ end
1086
+ return unless find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, name, zone) }
1087
+ closest = name.strip_label
1088
+ while within_zone?(closest, zone)
1089
+ break if find_denial_record(records) { |rr| within_zone?(rr.name, closest) || within_zone?(rr.next_domain, closest) }
1090
+ return if closest == zone
1091
+ closest = closest.strip_label
1092
+ end
1093
+ next_closer = Name.new(name.labels.last(closest.labels.length + 1), true)
1094
+ return unless find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, next_closer, zone) }
1095
+ wildcard = Name.new([Name::Label.new('*')] + closest.labels, true)
1096
+ if rcode == RCode.NXDOMAIN
1097
+ return :secure if find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, wildcard, zone) }
1098
+ else
1099
+ record = find_denial_record(records) { |rr| rr.name == wildcard }
1100
+ return :secure if record && denial_bitmap?(record, qtype)
1101
+ end
1102
+ nil
1103
+ end
480
1104
 
1105
+ def find_denial_record(records)
1106
+ records.find do |record|
1107
+ ValidationBudget.current&.check!(1)
1108
+ yield record
1109
+ end
481
1110
  end
482
1111
 
483
- def check_record_proves_no_wildcard(msg, nsec) # :nodoc:
484
- # Check that the NSEC goes from the SOA to a zone canonically after a wildcard
485
- # print "Checking wildcard proof for #{nsec.name}\n"
486
- soa_rrset = msg.authority.rrset(nsec.name, 'SOA')
487
- if (soa_rrset.length > 0)
488
- # print "Found SOA for #{nsec.name}\n"
489
- wildcard_name = Name.create("*." + nsec.name.to_s)
490
- # print "Checking #{wildcard_name}\n"
491
- if (wildcard_name.canonically_before(nsec.next_domain))
492
- return true
1112
+ def consistent_nsec_intervals?(records)
1113
+ # If an interval contains any observed owner, it contains the next
1114
+ # distinct owner in canonical order. Sorting avoids an all-pairs scan.
1115
+ owners = records.group_by do |rr|
1116
+ ValidationBudget.current&.check!(1)
1117
+ rr.name.canonical
1118
+ end.values.sort do |first, second|
1119
+ ValidationBudget.current&.check!(1)
1120
+ first.first.name.canonically_before(second.first.name) ? -1 : 1
1121
+ end
1122
+ owners.each_with_index do |group, index|
1123
+ following = owners[(index + 1) % owners.length].first.name
1124
+ group.each do |rr|
1125
+ ValidationBudget.current&.check!(1)
1126
+ return false if interval_covers?(rr.name, rr.next_domain, following)
493
1127
  end
494
1128
  end
495
- return false
1129
+ true
496
1130
  end
497
1131
 
498
- def check_name_in_nsecs(msg, qtype=nil, expected_qtype = false) # :nodoc:
499
- # Check these NSECs to make sure that this name cannot be in the zone
500
- # and that no RRSets could match through wildcard expansion
501
- # @TODO@ Get this right for NSEC3 too!
502
- name = msg.question()[0].qname
503
- proven_name_in_nsecs = false
504
- type_covered_checked = false
505
- [msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
506
- nsec_rrsets.each {|nsecs|
507
- nsecs.rrs.each {|nsec|
508
- # print "Checking NSEC : #{nsec}\n"
509
- next if (nsec.name.wild?)
510
- if nsec.check_name_in_range(name)
511
- proven_name_in_nsecs = true
512
- qtype_present = false
513
- if (qtype)
514
- if (nsec.types.include?qtype)
515
- qtype_present = true
516
- end
517
- if (qtype_present != expected_qtype)
518
- # print "#{nsec.type} record #{nsec} does #{expected_qtype ? 'not ' : ''} include #{qtype} type\n"
519
- raise VerifyError.new("#{nsec.type} record #{nsec} does #{expected_qtype ? 'not ' : ''}include #{qtype} type")
520
- # return false
521
- end
522
- type_covered_checked = true
523
- end
524
- end
525
- }
526
- }
527
- }
528
- if (!proven_name_in_nsecs)
529
- # print "No proof for non-existence for #{name}\n"
530
- raise VerifyError.new("No proof for non-existence for #{name}")
531
- end
532
- if (qtype && !type_covered_checked)
533
- # print "Tyes covered wrong for #{name}\n"
534
- raise VerifyError.new("Types covered wrong for #{name}")
535
- end
536
- end
537
-
538
- def check_name_in_and_type_not_in_nsecs(msg) # :nodoc:
539
- check_name_in_nsecs(msg, msg.question()[0].qtype, false)
540
- end
541
-
542
- def check_name_not_in_wildcard_nsecs(msg) # :nodoc:
543
- # @TODO@ Do this for NSEC3 records too!
544
- name = msg.question()[0].qname
545
- qtype = msg.question()[0].qtype
546
- done= false
547
- [msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
548
- nsec_rrsets.each {|nsecs|
549
- nsecs.rrs.each {|nsec|
550
- # print "Checking NSEC : #{nsec}\n"
551
- next if !nsec.name.wild?
552
- # Check the wildcard expansion
553
- # We want to see that the name is in the wildcard range, and that the type
554
- # is not in the types for the NSEC
555
- if nsec.check_name_in_wildcard_range(name)
556
- # print "Wildcard expansion in #{nsec} includes #{name}\n"
557
- raise VerifyError.new("Wildcard expansion in #{nsec} includes #{name}")
558
- # return false
559
- end
560
- if (nsec.types.include?qtype)
561
- # print "#{qtype} present in wildcard #{nsec}\n"
562
- raise VerifyError.new("#{qtype} present in wildcard #{nsec}")
563
- # return false
564
- end
565
- done = true
566
- }
567
- }
568
- }
569
- return if done
570
- # print("Expected wildcard expansion in #{msg}\n")
571
- raise VerifyError.new("Expected wildcard expansion in #{msg}")
572
- # return false
1132
+ # Section#rrsets intentionally returns copies. Publish their authenticated
1133
+ # bounds only after the entire message has passed validation, including its
1134
+ # denial proof, so the response cache receives the same lifetimes.
1135
+ def apply_verified_lifetimes(msg, context)
1136
+ now = Time.now.to_i
1137
+ context[:sections].each do |section, rrsets|
1138
+ limits = {}
1139
+ rrsets.each do |rrset|
1140
+ ValidationBudget.current&.check!(1)
1141
+ result = context[:verified][rrset]
1142
+ next unless result
1143
+ limits[[rrset.name.canonical, rrset.klass.code, rrset.type.code]] =
1144
+ [rrset.ttl, result[:expiration]]
1145
+ end
1146
+ # An unsigned Additional set must not inherit authentication from a
1147
+ # set with the same owner/class/type in another section.
1148
+ msg.public_send(section).each do |rr|
1149
+ ValidationBudget.current&.check!(1)
1150
+ type = rr.type == Types.RRSIG ? rr.type_covered : rr.type
1151
+ limit = limits[[rr.name.canonical, rr.klass.code, type.code]]
1152
+ next unless limit
1153
+ previous = rr.instance_variable_get(:@dnsruby_authenticated_expiration)
1154
+ deadline = [previous, limit.last].compact.min
1155
+ rr.ttl = [rr.ttl, limit.first, [deadline - now, 0].max].min
1156
+ rr.instance_variable_set(:@dnsruby_authenticated_expiration, deadline)
1157
+ rr.instance_variable_set(:@dnsruby_received_at, now)
1158
+ end
1159
+ end
1160
+ end
1161
+ private :terminal_answer, :within_zone?, :denial_bitmap?, :nsec3_denial,
1162
+ :nsec_denial, :apply_verified_lifetimes, :find_denial_record, :consistent_nsec_intervals?
1163
+
1164
+ def verify_ds_rrset(ds_rrset, keys = nil, msg = nil, context = {}) # :nodoc:
1165
+ with_validation { verify_ds_rrset_internal(ds_rrset, keys, msg, context) }
573
1166
  end
574
1167
 
575
- def verify_ds_rrset(ds_rrset, keys = nil, msg = nil) # :nodoc:
1168
+ private def verify_ds_rrset_internal(ds_rrset, keys, msg, context)
576
1169
  # print "verify_ds_rrset #{ds_rrset}\n"
577
1170
  if (ds_rrset && ds_rrset.num_sigs > 0)
578
- if (verify_rrset(ds_rrset, keys))
579
- # Need to handle DS RRSets (with RRSIGs) not just DS records.
580
- # ds_rrset.rrs.each do |ds|
581
- # Work out which key this refers to, and add it to the trusted key store
582
- found = false
583
- if (msg)
584
- msg.each_section do |section|
585
- section.rrsets('DNSKEY').each {|rrset|
586
- rrset.rrs.each do |rr|
587
- if (check_ds(rr, ds_rrset))
588
- found = true
589
- end
590
- end
591
- }
592
- end
593
- end
594
- get_keys_to_check().each {|key|
595
- if (check_ds(key, ds_rrset))
596
- found = true
1171
+ if (verify_rrset(ds_rrset, keys, context))
1172
+ return true if context[:insecure]
1173
+ provenance = context.fetch(:provenance)
1174
+ retain_learned_rrset(ds_rrset, provenance)
1175
+ if usable_provenance?(provenance)
1176
+ publish_trust do
1177
+ same_set = lambda { |set| set.name == ds_rrset.name && set.klass == ds_rrset.klass && set.type == ds_rrset.type }
1178
+ trust_operation[:ds].reject! { |set, _trust| same_set.call(set) }
1179
+ trust_operation[:ds] << [ds_rrset, provenance]
1180
+ @discovered_ds_store.add(ds_rrset, provenance.expiration)
597
1181
  end
598
- }
599
- # If we couldn't find the trusted key, then we should store the
600
- # key tag and digest in a @@discovered_ds_store.
601
- # Each time we see a new key (which has been signed) then we should
602
- # check if it is sitting on the discovered_ds_store.
603
- # If it is, then we should add it to the trusted_keys and remove the
604
- # DS from the discovered_ds_store
605
- if (!found)
606
- @discovered_ds_store.push(ds_rrset)
607
1182
  end
608
- # end
609
1183
  return true
610
1184
  else
611
1185
  return false
@@ -614,60 +1188,116 @@ module Dnsruby
614
1188
  return false # no DS rrset to verify
615
1189
  end
616
1190
 
617
- def verify_key_rrset(key_rrset, keys = nil) # :nodoc:
1191
+ def verify_key_rrset(key_rrset, keys = nil, context = {}) # :nodoc:
1192
+ with_validation { verify_key_rrset_internal(key_rrset, keys, context) }
1193
+ end
1194
+
1195
+ private def verify_key_rrset_internal(key_rrset, keys, context)
618
1196
  # print "verify_key_rrset\n"
619
1197
  verified = false
620
1198
  if (key_rrset && key_rrset.num_sigs > 0)
621
- if (verify_rrset(key_rrset, keys))
1199
+ if (verify_rrset(key_rrset, keys, context))
622
1200
  # key_rrset.rrs.each do |rr|
623
1201
  # print "Adding keys : "
624
1202
  # key_rrset.rrs.each {|rr| print "#{rr.key_tag}, "}
625
1203
  # print "\n"
626
- @trusted_keys.add(key_rrset) # rr)
1204
+ unless context[:insecure]
1205
+ retain_learned_rrset(key_rrset, context.fetch(:provenance))
1206
+ add_trusted_key(key_rrset, context.fetch(:expiration))
1207
+ end
627
1208
  verified = true
628
1209
  end
629
- check_ds_stores(key_rrset)
630
1210
  end
631
1211
  return verified
632
1212
  end
633
1213
 
634
- def check_ds_stores(key_rrset) # :nodoc:
635
- # See if the keys match any of the to_be_trusted_keys
1214
+ def check_ds_stores(key_rrset, signatures = nil) # :nodoc:
1215
+ with_validation { check_ds_stores_internal(key_rrset, signatures) }
1216
+ end
1217
+
1218
+ private def check_ds_stores_internal(key_rrset, signatures)
1219
+ signatures ||= valid_signatures(key_rrset)
1220
+ return if signatures.empty?
1221
+ # A digest match supplies a candidate key, not a cache lifetime. Publish
1222
+ # only after authenticating the DNSKEY RRset and its TTL.
1223
+ verify_key_rrset(key_rrset)
1224
+ end
1225
+
1226
+ def ds_candidate_keys(key_rrset, explicit_ds = nil)
1227
+ configured, discovered = @trust_mutex.synchronize do
1228
+ expire_trust_anchors
1229
+ [@configured_ds_index.matching_rrsets(key_rrset.rrs), @discovered_ds_store.matching_rrsets(key_rrset.rrs)]
1230
+ end
1231
+ stores = configured.map { |rrset| [nil, preferred_ds_records(rrset.rrs)] }
1232
+ stores.concat(discovered.filter_map do |rrset|
1233
+ provenance = learned_provenance(rrset)
1234
+ [admit_provenance(provenance), preferred_ds_records(rrset.rrs)] if usable_provenance?(provenance)
1235
+ end)
1236
+ stores.concat((trust_operation&.fetch(:ds) || []).filter_map do |rrset, provenance|
1237
+ [provenance, preferred_ds_records(rrset.rrs)] if usable_provenance?(provenance)
1238
+ end)
1239
+ if explicit_ds
1240
+ # Caller-supplied DS records are explicit trust input. If this is a
1241
+ # learned DS RRset or a new set wrapping learned records, retain their
1242
+ # original absolute deadlines and trust sources.
1243
+ provenance = admit_learned_rrset(explicit_ds)
1244
+ stores = preferred_ds_records(explicit_ds.rrs).filter_map do |record|
1245
+ trust = provenance || learned_provenance(record)
1246
+ next if trust && !usable_provenance?(trust)
1247
+ [admit_provenance(trust), [record]]
1248
+ end
1249
+ raise VerifyError, 'Learned DS trust is no longer valid' if stores.empty?
1250
+ end
1251
+ # Apply digest preference above to whole RRsets before indexing. Otherwise
1252
+ # an unrelated SHA-256 record could accidentally permit a SHA-1 fallback.
1253
+ indexed = {}
1254
+ stores.each do |provenance, records|
1255
+ records.each do |record|
1256
+ candidate = DSCache.candidate_identity(record)
1257
+ (indexed[candidate] ||= []) << [provenance, record]
1258
+ end
1259
+ end
1260
+ candidates = {}
636
1261
  key_rrset.rrs.each do |key|
637
- @configured_ds_store.each do |ds|
638
- if (ds.check_key(key))
639
- @trusted_keys.add_key_with_expiration(key, key_rrset.sigs()[0].expiration)
640
- end
1262
+ matches = [key.name.canonical, nil].flat_map do |owner|
1263
+ indexed[[owner, key.klass.code, key.algorithm.code, key.key_tag]] || []
641
1264
  end
642
- @discovered_ds_store.each do |tbtk|
643
- # Check that the RRSet is still valid!!
644
- # Should we get it out of the main cache?
645
- if ((tbtk.sigs()[0].expiration < Time.now.to_i))
646
- @discovered_ds_store.delete(tbtk)
647
- else
648
- tbtk.rrs.each {|ds|
649
- if (ds.check_key(key))
650
- @trusted_keys.add_key_with_expiration(key, tbtk.sigs()[0].expiration)
651
- @discovered_ds_store.delete(tbtk)
652
- end
653
- }
1265
+ matches.each do |provenance, ds|
1266
+ ValidationBudget.current&.check!(1)
1267
+ next unless ds.check_key(key)
1268
+ identity = KeyCache.key_identity(key)
1269
+ previous = candidates[identity]
1270
+ if !previous || (previous.last && (!provenance || provenance.expiration > previous.last.expiration))
1271
+ candidates[identity] = [key, provenance]
654
1272
  end
655
1273
  end
656
- # end
657
1274
  end
658
-
1275
+ candidates
659
1276
  end
1277
+ private :ds_candidate_keys
660
1278
 
661
- def get_keys_to_check # :nodoc:
662
- keys_to_check = @trust_anchors.keys + @trusted_keys.keys
663
- return keys_to_check
1279
+ def get_keys_to_check(signatures = nil) # :nodoc:
1280
+ anchors, keys = @trust_mutex.synchronize do
1281
+ expire_trust_anchors
1282
+ [signatures ? @trust_anchors.matching_keys(signatures) : @trust_anchors.keys,
1283
+ cached_trust_keys(signatures)]
1284
+ end
1285
+ local = (trust_operation&.fetch(:keys)&.values || []).filter_map do |key, provenance|
1286
+ key if usable_provenance?(provenance)
1287
+ end
1288
+ (anchors + keys.select { |key| usable_cached_key?(key) } + local).uniq { |key| KeyCache.key_identity(key) }
664
1289
  end
665
1290
 
666
1291
  # Find the first matching DNSKEY and RRSIG record in the two sets.
667
1292
  def get_matching_key(keys, sigrecs)#:nodoc: all
668
- # There can be multiple signatures in the RRSet - which one should we choose?
1293
+ each_matching_key(keys, sigrecs) { |key, sig| return key, sig }
1294
+ return nil, nil
1295
+ end
1296
+
1297
+ # A key tag is only a hint: every candidate must be cryptographically tried.
1298
+ def each_matching_key(keys, sigrecs)
669
1299
  if ((keys == nil) || (sigrecs == nil))
670
- return nil, nil
1300
+ return
671
1301
  end
672
1302
  if ((RR::DNSKEY === keys) || (RR::DS === keys) ||
673
1303
  ((RR::DLV === keys) && (@verifier_type == VerifierType::DLV)))
@@ -677,27 +1307,35 @@ module Dnsruby
677
1307
  if (enumerator.class == RRSet)
678
1308
  enumerator = enumerator.rrs
679
1309
  end
1310
+ signatures_by_key = sigrecs.group_by do |sig|
1311
+ KeyCache.candidate_identity(sig.signers_name, sig.klass, sig.algorithm, sig.key_tag)
1312
+ end
680
1313
  enumerator.each {|key|
1314
+ ValidationBudget.current&.check!(1)
1315
+ next if trust_operation&.fetch(:operational, false) && !algorithm_supported?(key.algorithm, operational: true)
681
1316
  if ((key.revoked?)) # || (key.bad_flags?))
682
1317
  next
683
1318
  end
684
1319
 
685
- sigrecs.each {|sig|
686
- # print "Looking at #{sig.key_tag} on sig, #{key.key_tag} on key\n"
687
- if ((key.key_tag == sig.key_tag) && (key.algorithm == sig.algorithm))
688
- # print "Found key #{key.key_tag}\n"
689
- return key, sig
690
- end
691
- }
1320
+ candidate = KeyCache.candidate_identity(key.name, key.klass, key.algorithm, key.key_tag)
1321
+ (signatures_by_key[candidate] || []).each do |sig|
1322
+ ValidationBudget.current&.check!(1)
1323
+ yield key, sig
1324
+ end
692
1325
  }
693
- return nil, nil
694
1326
  end
1327
+ private :each_matching_key
695
1328
 
696
1329
  # Verify the signature of an rrset encoded with the specified KeyCache
697
1330
  # or RRSet. If no signature is included, false is returned.
698
1331
  #
699
1332
  # Returns true if the RRSet verified, false otherwise.
700
- def verify_rrset(rrset, keys = nil)
1333
+ def verify_rrset(rrset, keys = nil, context = {})
1334
+ with_validation { verify_rrset_internal(rrset, keys, context) }
1335
+ end
1336
+
1337
+ private def verify_rrset_internal(rrset, keys, context)
1338
+ received_at = rrset.rrs.map { |rr| rr.instance_variable_get(:@dnsruby_received_at) }.compact.min || Time.now.to_i
701
1339
  # print "Verify_rrset #{rrset.name}, #{rrset.type}\n"
702
1340
  # print "ABOUT TO VERIFY WITH #{keys == nil ? '0' : keys.length} keys\n"
703
1341
  # if (keys != nil)
@@ -712,72 +1350,119 @@ module Dnsruby
712
1350
  if (rrset.num_sigs == 0)
713
1351
  raise VerifyError.new("No signatures in the RRSet : #{rrset.name}, #{rrset.type}")
714
1352
  end
715
- sigrecs.each do |sigrec|
716
- check_rr_data(rrset, sigrec)
717
- end
1353
+ sigrecs = valid_signatures(rrset)
718
1354
  raise ArgumentError.new("Expecting DNSKEY, DLV, DS, RRSet, Array or nil for keys : got #{keys.class} instead") if
719
1355
  (keys && (![Array, RR::IN::DNSKEY, RR::IN::DLV, RR::IN::DS].include?keys.class) && (keys.class != RRSet))
720
1356
 
721
- keyrec = nil
722
- sigrec = nil
723
- if (rrset.type == Types.DNSKEY)
724
- if (keys && !(Array === keys) && ((keys.type == Types.DS) || ((keys.type == Types.DLV) && (@verifier_type == VerifierType::DLV))))
725
- rrset.rrs.each do |key|
726
- keys.rrs.each do |ds|
727
- if (ds.check_key(key))
728
- @trusted_keys.add_key_with_expiration(key, rrset.sigs()[0].expiration)
729
- end
730
- end
731
- end
732
- else
733
- check_ds_stores(rrset)
734
- end
1357
+ proof_keys = keys
1358
+ explicit_keys = !keys.nil?
1359
+ explicit_provenance = admit_learned_rrset(keys)
1360
+ # Wrap a single DS or DLV record without losing its learned provenance.
1361
+ if [RR::IN::DS, RR::IN::DLV].include?(keys.class)
1362
+ keys = RRSet.new([keys])
1363
+ retain_learned_rrset(keys, explicit_provenance) if explicit_provenance
735
1364
  end
736
- if ((keys.nil?) || ((keys.class != Array) && ((keys.type == Types.DS) || ((keys.type == Types.DLV) && (@verifier_type == VerifierType::DLV)))))
737
- keyrec, sigrec = get_matching_key(get_keys_to_check, sigrecs)
738
- else
739
- keyrec, sigrec = get_matching_key(keys, sigrecs)
1365
+
1366
+ ds_keys = {}
1367
+ explicit_ds = keys if keys.is_a?(RRSet) &&
1368
+ (keys.type == Types.DS || (keys.type == Types.DLV && @verifier_type == VerifierType::DLV))
1369
+ if rrset.type == Types.DNSKEY && (keys.nil? || explicit_ds)
1370
+ ds_keys = ds_candidate_keys(rrset, explicit_ds)
1371
+ end
1372
+ if keys.nil? || explicit_ds
1373
+ keys = get_keys_to_check(sigrecs) + ds_keys.values.map(&:first)
740
1374
  end
741
1375
 
742
- # return false if !keyrec
743
- if (!keyrec)
744
- # print "Couldn't find signing key! #{rrset.name}, #{rrset.type},\n "
745
- raise VerifyError.new("Signing key not found")
1376
+ key_members = if rrset.type == Types.DNSKEY
1377
+ rrset.rrs.each_with_object({}) do |key, members|
1378
+ ValidationBudget.current&.check!(1)
1379
+ members[KeyCache.key_identity(key)] = true
1380
+ end
746
1381
  end
1382
+ error = VerifyError.new("Signing key not found")
1383
+ canonical_data = {}
1384
+ each_matching_key(keys, sigrecs) do |keyrec, sigrec|
1385
+ begin
1386
+ identity = KeyCache.key_identity(keyrec)
1387
+ if key_members && !key_members.key?(identity)
1388
+ raise VerifyError, 'Signing DNSKEY is absent from the apex DNSKEY RRSet'
1389
+ end
1390
+ parent_provenance = if explicit_ds
1391
+ ds_keys.key?(identity) ? ds_keys[identity].last : key_provenance(keyrec)
1392
+ elsif explicit_keys
1393
+ explicit_provenance || admit_learned_rrset(keyrec)
1394
+ elsif ds_keys.key?(identity)
1395
+ ds_keys[identity].last
1396
+ else
1397
+ key_provenance(keyrec)
1398
+ end
1399
+ next if parent_provenance && !usable_provenance?(parent_provenance)
1400
+ parent_provenance = admit_provenance(parent_provenance)
1401
+ ValidationBudget.current&.signature!
1402
+ ttl_expiration = received_at + [rrset.ttl, sigrec.ttl, sigrec.original_ttl].min
1403
+ previous_expirations = (rrset.rrs + [sigrec]).map do |rr|
1404
+ rr.instance_variable_get(:@dnsruby_authenticated_expiration)
1405
+ end.compact
1406
+ verify_rrset_with_key(rrset, keyrec, sigrec, canonical_data)
1407
+ check_wildcard_proof(rrset, sigrec, proof_keys, context) if context[:message]
1408
+ if parent_provenance && !usable_provenance?(parent_provenance)
1409
+ raise VerifyError, 'Learned trust expired during verification'
1410
+ end
1411
+ signature_expiration = SerialTime.absolute(sigrec.expiration, Time.now.to_i)
1412
+ raise VerifyError, 'Signature expiration is ambiguous' unless signature_expiration
1413
+ expiration = ([ttl_expiration, signature_expiration] + previous_expirations).min
1414
+ provenance = authenticated_provenance(rrset, sigrec, expiration, parent_provenance)
1415
+ verified_at = Time.now.to_i
1416
+ rrset.ttl = [rrset.ttl, [provenance.expiration - verified_at, 0].max].min
1417
+ rrset.each do |rr|
1418
+ rr.instance_variable_set(:@dnsruby_authenticated_expiration, provenance.expiration)
1419
+ rr.instance_variable_set(:@dnsruby_received_at, verified_at)
1420
+ end
1421
+ context[:expiration] = provenance.expiration
1422
+ context[:provenance] = provenance
1423
+ context[:signature] = sigrec
1424
+ context[:verified][rrset] = { signature: sigrec, expiration: provenance.expiration } if context[:verified]
1425
+ return true
1426
+ rescue VerifyError => e
1427
+ error = e
1428
+ rescue OpenSSL::OpenSSLError => e
1429
+ # Wire-decoded DNSKEYs may only be parsed by OpenSSL at this point.
1430
+ # A malformed colliding candidate must not mask a valid signing key.
1431
+ error = VerifyError.new("Signature failed to cryptographically verify: #{e.message}")
1432
+ end
1433
+ end
1434
+ raise error
1435
+ end
1436
+
1437
+ def algorithm_supported?(algorithm, operational: false)
1438
+ allowed = operational ? OPERATIONAL_ALGORITHMS : VALIDATION_ALGORITHMS
1439
+ allowed.include?(algorithm) &&
1440
+ (algorithm != Algorithms.ED25519 || RR::DNSKEY.ed25519_supported?) &&
1441
+ (algorithm != Algorithms.ED448 || RR::DNSKEY.ed448_supported?)
1442
+ end
1443
+ private :algorithm_supported?
747
1444
 
1445
+ def verify_rrset_with_key(rrset, keyrec, sigrec, canonical_data = {})
748
1446
  # RFC 4034
749
1447
  # 3.1.8.1. Signature Calculation
750
1448
 
751
- if (keyrec.sep_key? && !keyrec.zone_key?)
752
- Dnsruby.log.error("DNSKEY with SEP flag set and Zone Key flag not set was used to verify RRSIG over RRSET - this is not allowed by RFC4034 section 2.1.1")
753
- # return false
754
- raise VerifyError.new("DNSKEY with SEP flag set and Zone Key flag not set")
1449
+ unless keyrec.zone_key?
1450
+ raise VerifyError, 'DNSKEY Zone Key flag is not set'
755
1451
  end
756
-
757
-
758
- # print "VERIFY KEY FOUND - doing verification\n"
759
-
760
- # Any DNS names in the RDATA field of each RR MUST be in
761
- # canonical form; and
762
- # The RRset MUST be sorted in canonical order.
763
- rrset = rrset.sort_canonical
764
-
765
- sig_data = sigrec.sig_data
766
-
767
- # RR(i) = owner | type | class | TTL | RDATA length | RDATA
768
- rrset.each do |rec|
769
- old_ttl = rec.ttl
770
- rec.ttl = sigrec.original_ttl
771
- data = MessageEncoder.new { |msg|
772
- msg.put_rr(rec, true)
773
- }.to_s # @TODO@ worry about wildcards here?
774
- rec.ttl = old_ttl
775
- if (RUBY_VERSION >= "1.9")
776
- data.force_encoding(Encoding::BINARY)
1452
+ unless algorithm_supported?(sigrec.algorithm)
1453
+ if [Algorithms.DSA, Algorithms.DSA_NSEC3_SHA1].include?(sigrec.algorithm)
1454
+ raise VerifyError, "Algorithm #{sigrec.algorithm.code} is prohibited for DNSSEC validation"
777
1455
  end
778
- sig_data += data
1456
+ raise VerifyError, "Algorithm #{sigrec.algorithm.code} unsupported by Dnsruby"
779
1457
  end
780
1458
 
1459
+ # Canonical RDATA is independent of the signature. The RR wire body only
1460
+ # varies with Original TTL and wildcard label count (RFC 4034 section 3.1.8).
1461
+ # Keep it local to this verification so mutable caller records are never
1462
+ # cached across calls, and do not temporarily mutate their TTLs.
1463
+ sig_data = sigrec.sig_data + canonical_rrset_data(rrset, sigrec, canonical_data)
1464
+ ValidationBudget.current&.check!(sig_data.bytesize / 64 + 1)
1465
+
781
1466
  # Now calculate the signature
782
1467
  verified = false
783
1468
  if [Algorithms.RSASHA1,
@@ -787,20 +1472,17 @@ module Dnsruby
787
1472
  verified = keyrec.public_key.verify(OpenSSL::Digest::SHA256.new, sigrec.signature, sig_data)
788
1473
  elsif (sigrec.algorithm == Algorithms.RSASHA512)
789
1474
  verified = keyrec.public_key.verify(OpenSSL::Digest::SHA512.new, sigrec.signature, sig_data)
790
- elsif [Algorithms.DSA,
791
- Algorithms.DSA_NSEC3_SHA1].include?(sigrec.algorithm)
792
- # we are ignoring T for now
793
- # t = sigrec.signature[0]
794
- # t = t.getbyte(0) if t.class == String
795
- r = RR::get_num(sigrec.signature[1, 20])
796
- s = RR::get_num(sigrec.signature[21, 20])
797
- r_asn1 = OpenSSL::ASN1::Integer.new(r)
798
- s_asn1 = OpenSSL::ASN1::Integer.new(s)
799
-
800
- asn1 = OpenSSL::ASN1::Sequence.new([r_asn1, s_asn1]).to_der
801
- verified = keyrec.public_key.verify(OpenSSL::Digest::DSS1.new, asn1, sig_data)
1475
+ elsif [Algorithms.ED25519, Algorithms.ED448].include?(sigrec.algorithm)
1476
+ size = sigrec.algorithm == Algorithms.ED25519 ? 64 : 114
1477
+ unless sigrec.signature.bytesize == size
1478
+ raise VerifyError, "#{sigrec.algorithm} signature must contain #{size} octets"
1479
+ end
1480
+ verified = keyrec.public_key.verify(nil, sigrec.signature, sig_data)
802
1481
  elsif [Algorithms.ECDSAP256SHA256, Algorithms.ECDSAP384SHA384].include?(sigrec.algorithm)
803
1482
  byte_size = (keyrec.public_key.group.degree + 7) / 8
1483
+ if sigrec.signature.bytesize != 2 * byte_size
1484
+ raise VerifyError, "Invalid ECDSA signature length"
1485
+ end
804
1486
  sig_bytes = sigrec.signature[0..(byte_size - 1)]
805
1487
  sig_char = sigrec.signature[byte_size..-1] || ''
806
1488
  asn1 = OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
@@ -813,21 +1495,161 @@ module Dnsruby
813
1495
 
814
1496
  verified = keyrec.public_key.dsa_verify_asn1(digest_obj.digest(sig_data), asn1)
815
1497
  else
816
- raise RuntimeError.new("Algorithm #{sigrec.algorithm.code} unsupported by Dnsruby")
1498
+ raise VerifyError.new("Algorithm #{sigrec.algorithm.code} unsupported by Dnsruby")
817
1499
  end
818
1500
 
819
1501
  if (!verified)
820
1502
  raise VerifyError.new("Signature failed to cryptographically verify")
821
1503
  end
822
- # Sort out the TTLs - set it to the minimum valid ttl
823
- expiration_diff = (sigrec.expiration.to_i - Time.now.to_i).abs
824
- rrset.ttl = ([rrset.ttl, sigrec.ttl, sigrec.original_ttl,
825
- expiration_diff].sort)[0]
1504
+ ValidationBudget.current&.check!
1505
+ # Commit lifetime bounds only after the wildcard proof also succeeds.
1506
+ unless SerialTime.valid?(sigrec.inception, sigrec.expiration)
1507
+ raise VerifyError, 'Signature expired during verification'
1508
+ end
826
1509
  # print "VERIFIED OK\n"
827
1510
  return true
828
1511
  end
1512
+ private :verify_rrset_with_key
1513
+
1514
+ def canonical_rrset_data(rrset, signature, cache)
1515
+ identity = [signature.original_ttl, signature.labels]
1516
+ return cache[identity] if cache.key?(identity)
1517
+ name = rrset.name
1518
+ if signature.labels < 0 || signature.labels > name.labels.length
1519
+ raise VerifyError, 'Invalid RRSIG label count'
1520
+ elsif signature.labels < name.labels.length
1521
+ name = Name.new([Name::Label.new('*')] + name.labels.last(signature.labels), true)
1522
+ end
1523
+ rdatas = cache[:rdatas] ||= rrset.rrs.map do |record|
1524
+ ValidationBudget.current&.check!(1)
1525
+ data = MessageEncoder.new { |encoder| record.encode_rdata(encoder, true) }.to_s
1526
+ ValidationBudget.current&.check!(data.bytesize / 64 + 1)
1527
+ data
1528
+ end.uniq.sort
1529
+ prefix = MessageEncoder.new do |encoder|
1530
+ encoder.put_name(name, true)
1531
+ encoder.put_pack('nnN', rrset.type.code, rrset.klass.code, signature.original_ttl)
1532
+ end.to_s
1533
+ cache[identity] = rdatas.map do |data|
1534
+ ValidationBudget.current&.check!(1)
1535
+ prefix + [data.bytesize].pack('n') + data
1536
+ end.join
1537
+ end
1538
+ private :canonical_rrset_data
1539
+
1540
+ # A valid wildcard signature authenticates the source wildcard, not the
1541
+ # expanded owner. RFC 4035 5.3.4 / RFC 5155 8.8 also require denial of the
1542
+ # next-closer name. Keep this at message level: verify_rrset alone has no
1543
+ # response from which to obtain that proof.
1544
+ def check_wildcard_proof(rrset, signature, keys, context)
1545
+ labels = signature.labels
1546
+ return if labels == rrset.name.labels.length
1547
+ return if rrset.name.wild? && labels == rrset.name.labels.length - 1
1548
+ zone = signature.signers_name
1549
+ closest = Name.new(rrset.name.labels.last(labels), true)
1550
+ next_closer = Name.new(rrset.name.labels.last(labels + 1), true)
1551
+ unless closest == zone || closest.subdomain_of?(zone)
1552
+ raise VerifyError, 'Wildcard source is outside the signing zone'
1553
+ end
1554
+ proof_id = [zone.canonical, rrset.klass.code, next_closer.canonical]
1555
+ if (cached = context[:proofs][proof_id])
1556
+ context[:insecure] = true if cached == :insecure
1557
+ return
1558
+ end
1559
+
1560
+ authority = context[:sections].fetch('authority')
1561
+ insecure_proof = false
1562
+ authority.each do |proof|
1563
+ ValidationBudget.current&.check!(1)
1564
+ next unless [Types.NSEC, Types.NSEC3].include?(proof.type)
1565
+ next unless proof.klass == rrset.klass
1566
+ covers = proof.rrs.any? do |record|
1567
+ if record.type == Types.NSEC
1568
+ nsec_covers_next_closer?(record, next_closer, zone)
1569
+ else
1570
+ nsec3_covers_next_closer?(record, next_closer, zone)
1571
+ end
1572
+ end
1573
+ next unless covers
1574
+ # Only signatures from the wildcard's zone can authenticate its proof.
1575
+ # An expanded NSEC itself cannot prove the absence of its new owner.
1576
+ signatures = proof.sigs.select do |sig|
1577
+ sig.signers_name == zone && (sig.labels == proof.name.labels.length ||
1578
+ (proof.name.wild? && sig.labels == proof.name.labels.length - 1))
1579
+ end
1580
+ next if signatures.empty?
1581
+ begin
1582
+ verified = context[:verified][proof]
1583
+ unless verified && signatures.include?(verified[:signature])
1584
+ proof_context = {}
1585
+ restricted = RRSet.new(proof.rrs + signatures)
1586
+ verify_rrset(restricted, keys, proof_context)
1587
+ proof.ttl = [proof.ttl, restricted.ttl].min
1588
+ context[:verified][proof] = {
1589
+ signature: proof_context.fetch(:signature),
1590
+ expiration: proof_context.fetch(:expiration)
1591
+ }
1592
+ end
1593
+ if proof.type == Types.NSEC3 && proof.rrs.any? { |rr| rr.usable_hash? && rr.flags == 1 }
1594
+ insecure_proof = true
1595
+ next
1596
+ end
1597
+ context[:proofs][proof_id] = :secure
1598
+ return
1599
+ rescue VerifyError
1600
+ # Another authenticated denial record may supply the proof.
1601
+ end
1602
+ end
1603
+ if insecure_proof
1604
+ context[:proofs][proof_id] = :insecure
1605
+ context[:insecure] = true
1606
+ return
1607
+ end
1608
+ raise VerifyError, "No authenticated wildcard denial proof for #{rrset.name}"
1609
+ end
1610
+
1611
+ def nsec_covers_next_closer?(record, name, zone)
1612
+ owner, following = record.name, record.next_domain
1613
+ return false unless [owner, following].all? { |n| n == zone || n.subdomain_of?(zone) }
1614
+ # A descendant proves an empty nonterminal exists. Parent-side denial
1615
+ # below a delegation or DNAME cannot establish absence in that subtree.
1616
+ return false if following.subdomain_of?(name) || owner.subdomain_of?(name)
1617
+ if name.subdomain_of?(owner)
1618
+ return false if record.types.include?(Types.DNAME) ||
1619
+ (record.types.include?(Types.NS) && !record.types.include?(Types.SOA))
1620
+ end
1621
+ interval_covers?(owner, following, name)
1622
+ end
1623
+
1624
+ def nsec3_covers_next_closer?(record, name, zone)
1625
+ return false unless record.name.strip_label == zone && record.hash_alg == Nsec3HashAlgorithms.SHA_1
1626
+ # An Opt-Out interval can hide an insecure delegation, so cannot prove
1627
+ # this wildcard expansion secure (RFC 5155 section 9.2).
1628
+ return false unless [0, 1].include?(record.flags) && record.next_hashed.bytesize == 20
1629
+ owner = record.name.labels.first.to_s.downcase
1630
+ return false unless /\A[0-9a-v]{32}\z/.match?(owner)
1631
+ following = RR::NSEC3.encode_next_hashed(record.next_hashed).downcase
1632
+ hashed = RR::NSEC3.calculate_hash(name, record.iterations, RR::NSEC3.decode_salt(record.salt), record.hash_alg)
1633
+ interval_covers?(owner, following, hashed)
1634
+ end
1635
+
1636
+ def interval_covers?(first, following, name)
1637
+ return false if name == first || name == following
1638
+ if first.is_a?(Name)
1639
+ first.canonically_before(following) ?
1640
+ first.canonically_before(name) && name.canonically_before(following) :
1641
+ first.canonically_before(name) || name.canonically_before(following)
1642
+ else
1643
+ first < following ? first < name && name < following : first < name || name < following
1644
+ end
1645
+ end
1646
+ private :check_wildcard_proof, :nsec_covers_next_closer?, :nsec3_covers_next_closer?, :interval_covers?
829
1647
 
830
1648
  def find_closest_dlv_anchor_for(name) # :nodoc:
1649
+ with_validation { find_closest_dlv_anchor_for_internal(name) }
1650
+ end
1651
+
1652
+ private def find_closest_dlv_anchor_for_internal(name)
831
1653
  # To find the closest anchor, query DLV.isc.org for [a.b.c.d], then [a.b.c], [a.b], etc.
832
1654
  # once closest anchor found, simply run follow_chain from that anchor
833
1655
 
@@ -850,6 +1672,10 @@ module Dnsruby
850
1672
  end
851
1673
 
852
1674
  def get_zone_key_from_dlv_rrset(dlv_rrset, name) # :nodoc:
1675
+ with_validation { get_zone_key_from_dlv_rrset_internal(dlv_rrset, name) }
1676
+ end
1677
+
1678
+ private def get_zone_key_from_dlv_rrset_internal(dlv_rrset, name)
853
1679
  # We want to return the key for the zone i.e. DS/DNSKEY for .se, NOT DLV for se.dlv.isc.org
854
1680
  # So, we have the DLv record. Now use it to add the zone's DNSKEYs to the trusted key set.
855
1681
  res = get_nameservers_for(name)
@@ -874,6 +1700,8 @@ module Dnsruby
874
1700
  if (!ret)
875
1701
  raise ResolvError.new("Couldn't get DNSKEY from Recursor")
876
1702
  end
1703
+ rescue RecursionLimitError
1704
+ raise
877
1705
  rescue ResolvError => e
878
1706
  # print "Error getting zone key from DLV RR for #{name} : #{e}\n"
879
1707
  TheLog.error("Error getting zone key from DLV RR for #{name} : #{e}")
@@ -892,6 +1720,10 @@ module Dnsruby
892
1720
  end
893
1721
 
894
1722
  def query_dlv_for(name) # :nodoc:
1723
+ with_validation { query_dlv_for_internal(name) }
1724
+ end
1725
+
1726
+ private def query_dlv_for_internal(name)
895
1727
  # See if there is a record for name in dlv.isc.org
896
1728
  if (!@res && (@verifier_type == VerifierType::DLV))
897
1729
  @res = get_dlv_resolver
@@ -908,6 +1740,8 @@ module Dnsruby
908
1740
  if (!ret)
909
1741
  raise ResolvError.new("Couldn't get DLV record from Recursor")
910
1742
  end
1743
+ rescue RecursionLimitError
1744
+ raise
911
1745
  rescue ResolvError => e
912
1746
  # print "Error getting DLV record for #{name} : #{e}\n"
913
1747
  TheLog.info("Error getting DLV record for #{name} : #{e}")
@@ -932,22 +1766,40 @@ module Dnsruby
932
1766
  end
933
1767
 
934
1768
  def find_closest_anchor_for(name) # :nodoc:
935
- # Check if we have an anchor for name.
936
- # If not, strip off first label and try again
937
- # If we get to root, then return false
938
- name = "." if name == ""
939
- n = Name.create(name)
940
- root = Name.create(".")
941
- while (true) # n != root)
942
- # Try the trusted keys first, then the DS set
943
- (@trust_anchors.keys + @trusted_keys.keys + @configured_ds_store + @discovered_ds_store).each {|key|
944
- return key if key.name.canonical == n.canonical
945
- }
946
- break if (n.to_s == root.to_s)
947
- # strip the name
948
- n = n.strip_label
1769
+ n = Name.create(name == "" ? "." : name)
1770
+ names = [n]
1771
+ names << names.last.strip_label until names.last.labels.empty?
1772
+ # Lookup by owner before consulting provenance. A cache of unrelated
1773
+ # zones should not add work to every step down a delegation chain.
1774
+ candidates = @trust_mutex.synchronize do
1775
+ expire_trust_anchors
1776
+ names.map do |owner|
1777
+ [@trust_anchors.keys_for_name(owner) + @configured_ds_index.rrsets_for_name(owner),
1778
+ @trusted_keys.keys_for_name(owner), @discovered_ds_store.rrsets_for_name(owner)]
1779
+ end
949
1780
  end
950
- return false
1781
+ operation = trust_operation
1782
+ names.zip(candidates).each do |owner, (configured, keys, discovered)|
1783
+ canonical = owner.canonical
1784
+ anchors = configured + keys.select { |key| usable_cached_key?(key) } +
1785
+ discovered.select { |rrset| usable_provenance?(learned_provenance(rrset)) }
1786
+ if operation
1787
+ operation[:keys].each_value do |key, trust|
1788
+ anchors << key if key.name.canonical == canonical && usable_provenance?(trust)
1789
+ end
1790
+ operation[:ds].each do |rrset, trust|
1791
+ anchors << rrset if rrset.name.canonical == canonical && usable_provenance?(trust)
1792
+ end
1793
+ end
1794
+ if operation&.fetch(:operational, false)
1795
+ anchors.reject! { |key| key.type == Types.DNSKEY && !algorithm_supported?(key.algorithm, operational: true) }
1796
+ end
1797
+ # Records remain mutable; an index entry must not grant trust under
1798
+ # the old owner after the caller changes the record's name.
1799
+ anchor = anchors.find { |key| key.name.canonical == canonical }
1800
+ return anchor if anchor
1801
+ end
1802
+ false
951
1803
  end
952
1804
 
953
1805
  # @TODO@ Handle REVOKED keys! (RFC 5011)
@@ -956,6 +1808,19 @@ module Dnsruby
956
1808
  # that authority and remove any keys with the pre-revoked key_tag.
957
1809
 
958
1810
  def follow_chain(anchor, name) # :nodoc:
1811
+ with_validation do
1812
+ operation = trust_operation
1813
+ operation[:last_insecure_expiration] = nil
1814
+ result = follow_chain_internal(anchor, name)
1815
+ if !result && operation[:insecurity_expirations]
1816
+ operation[:insecurity_expirations] << operation[:last_insecure_expiration]
1817
+ end
1818
+ result
1819
+ end
1820
+ end
1821
+
1822
+ private def follow_chain_internal(anchor, name)
1823
+ admit_learned_rrset(anchor)
959
1824
  # Follow the chain from the anchor to name, returning the appropriate
960
1825
  # key at the end, or false.
961
1826
  #
@@ -972,6 +1837,7 @@ module Dnsruby
972
1837
  res = Dnssec.default_resolver
973
1838
  # while ((next_step != name) || (next_key.type != Types.DNSKEY))
974
1839
  while (true)
1840
+ ValidationBudget.current&.check!(1)
975
1841
  # print "In loop for parent=#{parent}, next step = #{next_step}\n"
976
1842
  dont_move_on = false
977
1843
  if (next_key.type != Types.DNSKEY)
@@ -985,7 +1851,7 @@ module Dnsruby
985
1851
  return false if (!next_key)
986
1852
  # Add the next label on
987
1853
  if (!dont_move_on)
988
- parent = next_step
1854
+ parent = next_key.name
989
1855
  next_step = Name.new(name.labels[name.labels.length-1-next_step.labels.length,1] +
990
1856
  next_step.labels , name.absolute?)
991
1857
  # print "Next parent = #{parent}, next_step = #{next_step}, next_key.type = #{next_key.type.string}\n"
@@ -998,6 +1864,21 @@ module Dnsruby
998
1864
  end
999
1865
 
1000
1866
  def get_anchor_for(child, parent, current_anchor, parent_res = nil) # :nodoc:
1867
+ with_validation { get_anchor_for_internal(child, parent, current_anchor, parent_res) }
1868
+ end
1869
+
1870
+ private def get_anchor_for_internal(child, parent, current_anchor, parent_res)
1871
+ admit_learned_rrset(current_anchor)
1872
+ if current_anchor.type == Types.DS
1873
+ records = current_anchor.is_a?(RRSet) ? current_anchor.rrs : [current_anchor]
1874
+ # A cached or configured DS is already trusted input, but its learned
1875
+ # provenance must still be live before its algorithm policy is used.
1876
+ unless preferred_ds_records(records).any? { |record| supported_ds?(record) }
1877
+ trust_operation[:last_insecure_expiration] = learned_provenance(current_anchor)&.expiration
1878
+ return false, parent_res
1879
+ end
1880
+ end
1881
+ ds_rrset = current_anchor
1001
1882
  # print "Trying to discover anchor for #{child} from #{parent}\n"
1002
1883
  TheLog.debug("Trying to discover anchor for #{child} from #{parent} using #{current_anchor}, #{parent_res}")
1003
1884
  # We wish to return a DNSKEY which the caller can use to verify name
@@ -1031,7 +1912,6 @@ module Dnsruby
1031
1912
  parent_res.dnssec = true
1032
1913
  end
1033
1914
  # Use that Resolver to query for DS record and NS for children
1034
- ds_rrset = current_anchor
1035
1915
  if (current_anchor.type == Types.DNSKEY)
1036
1916
  # print "Trying to find DS records for #{child} from servers for #{parent}\n"
1037
1917
  TheLog.debug("Trying to find DS records for #{child} from servers for #{parent}")
@@ -1041,23 +1921,36 @@ module Dnsruby
1041
1921
  if (!ds_ret)
1042
1922
  raise ResolvError.new("Couldn't get DS records from Recursor")
1043
1923
  end
1924
+ rescue NXDomain => e
1925
+ # NXDOMAIN is usable only with its authenticated denial response.
1926
+ ds_ret = e.response
1927
+ raise VerifyError, "Missing DS denial response for #{child}" unless ds_ret
1928
+ rescue RecursionLimitError
1929
+ raise
1044
1930
  rescue ResolvError => e
1045
- # print "Error getting DS record for #{child} : #{e}\n"
1046
- TheLog.error("Error getting DS record for #{child} : #{e}")
1047
- return false, nil
1931
+ raise VerifyError, "Error getting DS record for #{child}: #{e}"
1048
1932
  end
1049
1933
  ds_rrset = ds_ret.answer.rrset(child, Types.DS)
1050
1934
  if (ds_rrset.rrs.length == 0)
1051
- # @TODO@ Check NSEC(3) records - still need to verify there are REALLY no ds records!
1052
- # print "NO DS RECORDS RETURNED FOR #{parent}\n"
1053
- # child_res = parent_res
1935
+ denial = authenticated_ds_absence(ds_ret, child, current_anchor)
1936
+ # A label need not be a zone cut. Continue using the current
1937
+ # zone's key unless the proof establishes an unsigned delegation.
1938
+ return denial == :insecure ? [false, parent_res] : [current_anchor, parent_res]
1054
1939
  else
1055
1940
  begin
1056
- if (verify(ds_rrset, current_anchor) || verify(ds_rrset))
1057
- # Try to make the resolver from the authority/additional NS RRSets in DS response
1058
- if (!Dnssec.do_validation_with_recursor?)
1059
- child_res = get_nameservers_from_message(child, ds_ret)
1060
- end
1941
+ unless verify(ds_rrset, current_anchor) || verify(ds_rrset)
1942
+ raise VerifyError, "Unsigned DS RRSet for #{child}"
1943
+ end
1944
+ # RFC 4035 5.2: a securely authenticated delegation whose DS
1945
+ # records all use unavailable algorithms/digests is insecure.
1946
+ # Mixed sets must still establish trust using a supported DS.
1947
+ unless preferred_ds_records(ds_rrset.rrs).any? { |ds| supported_ds?(ds) }
1948
+ trust_operation[:last_insecure_expiration] = learned_provenance(ds_rrset)&.expiration
1949
+ return false, parent_res
1950
+ end
1951
+ # Try the child nameservers only after authenticating its DS.
1952
+ unless Dnssec.do_validation_with_recursor?
1953
+ child_res = get_nameservers_from_message(child, ds_ret)
1061
1954
  end
1062
1955
  rescue VerifyError => e
1063
1956
  # print "FAILED TO VERIFY DS RRSET FOR #{child}\n"
@@ -1102,39 +1995,24 @@ module Dnsruby
1102
1995
  if (!key_ret)
1103
1996
  raise ResolvError.new("Couldn't get info from Recursor")
1104
1997
  end
1998
+ rescue RecursionLimitError
1999
+ raise
1105
2000
  rescue ResolvError => e
1106
2001
  # print "Error getting DNSKEY for #{child} : #{e}\n"
1107
2002
  TheLog.error("Error getting DNSKEY for #{child} : #{e}")
1108
2003
  # return false, nil
1109
2004
  raise VerifyError.new("Error getting DNSKEY for #{child} : #{e}")
1110
2005
  end
1111
- verified = true
1112
2006
  key_rrset = key_ret.answer.rrset(child, Types.DNSKEY)
1113
- if (key_rrset.rrs.length == 0)
1114
- # @TODO@ Still need to check NSEC records to make *sure* no key rrs returned!
1115
- # print "NO DNSKEY RECORDS RETURNED FOR #{child}\n"
1116
- TheLog.debug("NO DNSKEY RECORDS RETURNED FOR #{child}")
1117
- # end
1118
- verified = false
1119
- else
1120
- # Should check that the matching key's zone flag is set (RFC 4035 section 5.2)
1121
- key_rrset.rrs.each {|k|
1122
- if (!k.zone_key?)
1123
- # print "Discovered DNSKEY is not a zone key - ignoring\n"
1124
- TheLog.debug("Discovered DNSKEY is not a zone key - ignoring")
1125
- return false, child_res
1126
- end
1127
- }
1128
- begin
1129
- verify(key_rrset, ds_rrset)
1130
- rescue VerifyError => e
1131
- begin
1132
- verify(key_rrset)
1133
- rescue VerifyError =>e
1134
- verified = false
1135
- raise VerifyError.new("Couldn't verify DNSKEY and DS records")
1136
- end
1137
- end
2007
+ if key_rrset.rrs.empty?
2008
+ raise VerifyError, "Missing DNSKEY RRSet for #{child}"
2009
+ end
2010
+ begin
2011
+ raise VerifyError, "Unsigned DNSKEY RRSet for #{child}" unless verify(key_rrset, ds_rrset)
2012
+ rescue VerifyError
2013
+ # Another explicitly configured or already learned anchor can still
2014
+ # authenticate this same zone. Non-zone keys are ignored as signers.
2015
+ raise VerifyError, "Couldn't verify DNSKEY and DS records for #{child}" unless verify(key_rrset)
1138
2016
  end
1139
2017
 
1140
2018
  # Try to make the resolver from the authority/additional NS RRSets in DNSKEY response
@@ -1142,11 +2020,6 @@ module Dnsruby
1142
2020
  if (!new_res)
1143
2021
  new_res = child_res
1144
2022
  end
1145
- if (!verified)
1146
- TheLog.info("Failed to verify DNSKEY for #{child}")
1147
- return false, nil # new_res
1148
- # raise VerifyError.new("Failed to verify DNSKEY for #{child}")
1149
- end
1150
2023
  # Cache.add(key_ret)
1151
2024
  return key_rrset, new_res
1152
2025
  rescue VerifyError => e
@@ -1157,6 +2030,67 @@ module Dnsruby
1157
2030
  end
1158
2031
  end
1159
2032
 
2033
+ def supported_ds?(record)
2034
+ algorithm_supported?(record.algorithm, operational: true) && [1, 2, 4].include?(record.digest_type.code)
2035
+ end
2036
+ private :supported_ds?
2037
+
2038
+ # Apply digest preference to the complete authenticated/trusted RRset,
2039
+ # before matching individual keys (RFC 4509 section 3).
2040
+ def preferred_ds_records(records)
2041
+ return records unless records.any? { |record| record.digest_type.code == 2 }
2042
+ records.reject { |record| record.digest_type.code == 1 }
2043
+ end
2044
+ private :preferred_ds_records
2045
+
2046
+ # Authenticate denial against the current parent key before classifying
2047
+ # an unsigned delegation. A successful denial at an ordinary name simply
2048
+ # means there is no zone cut at this label (RFC 4035 5.2 / RFC 5155 8.9).
2049
+ def authenticated_ds_absence(message, child, anchor)
2050
+ question = message.question.first
2051
+ unless question && question.qname == child && question.qtype == Types.DS &&
2052
+ question.qclass == anchor.klass && [RCode.NOERROR, RCode.NXDOMAIN].include?(message.rcode)
2053
+ raise VerifyError, "Unexpected DS denial response for #{child}"
2054
+ end
2055
+ zone = anchor.name
2056
+ groups = {}
2057
+ expirations = {}
2058
+ (message.authority.rrsets(Types.NSEC) + message.authority.rrsets(Types.NSEC3)).each do |proof|
2059
+ ValidationBudget.current&.check!(1)
2060
+ next unless proof.klass == anchor.klass
2061
+ signatures = proof.sigs.select do |signature|
2062
+ signature.signers_name == zone && (signature.labels == proof.name.labels.length ||
2063
+ (proof.name.wild? && signature.labels == proof.name.labels.length - 1))
2064
+ end
2065
+ next if signatures.empty?
2066
+ begin
2067
+ verification = {}
2068
+ verify_rrset(RRSet.new(proof.rrs + signatures), anchor, verification)
2069
+ (groups[proof.type] ||= []).concat(proof.rrs)
2070
+ (expirations[proof.type] ||= []) << verification[:expiration]
2071
+ rescue VerifyError
2072
+ # A second proof/signature may still provide valid denial.
2073
+ end
2074
+ end
2075
+ groups.each do |type, records|
2076
+ check = type == Types.NSEC3 ? :nsec3_denial : :nsec_denial
2077
+ if message.rcode == RCode.NOERROR &&
2078
+ send(check, records, child, Types.DS, zone, message.rcode, true) == :insecure
2079
+ operation = trust_operation
2080
+ operation[:last_insecure_expiration] = expirations.fetch(type).compact.min if operation
2081
+ return :insecure
2082
+ end
2083
+ result = send(check, records, child, Types.DS, zone, message.rcode, false)
2084
+ if result == :insecure
2085
+ operation = trust_operation
2086
+ operation[:last_insecure_expiration] = expirations.fetch(type).compact.min if operation
2087
+ end
2088
+ return result if result
2089
+ end
2090
+ raise VerifyError, "No authenticated DS denial proof for #{child}"
2091
+ end
2092
+ private :authenticated_ds_absence
2093
+
1160
2094
  def get_nameservers_for(name, res = nil) # :nodoc:
1161
2095
  # @TODO@ !!!
1162
2096
  if (Dnssec.do_validation_with_recursor?)
@@ -1238,51 +2172,88 @@ module Dnsruby
1238
2172
  end
1239
2173
 
1240
2174
  def validate_no_rrsigs(msg) # :nodoc:
1241
- # print "Validating unsigned response\n"
1242
- # WHAT IF THERE ARE NO RRSIGS IN MSG?
1243
- # Then we need to check that we do not expect any RRSIGs
2175
+ with_validation { validate_no_rrsigs_internal(msg) }
2176
+ end
2177
+
2178
+ private def validate_no_rrsigs_internal(msg)
1244
2179
  if (!msg.question()[0] && msg.answer.length == 0)
1245
- # print "Returning Message insecure OK\n"
1246
2180
  msg.security_level = Message::SecurityLevel.INSECURE
1247
2181
  return true
1248
2182
  end
1249
- qname = msg.question()[0].qname
1250
- closest_anchor = find_closest_anchor_for(qname)
1251
- # print "Found closest anchor :#{closest_anchor}\n"
1252
- if (closest_anchor)
1253
- actual_anchor = follow_chain(closest_anchor, qname)
1254
- # print "Actual anchor : #{actual_anchor}\n"
1255
- if (actual_anchor)
1256
- # print("Anchor exists for #{qname}, but no signatures in #{msg}\n")
1257
- TheLog.error("Anchor exists for #{qname}, but no signatures in #{msg}")
1258
- msg.security_level = Message::SecurityLevel.BOGUS
1259
- return false
1260
- end
2183
+ # An unsigned alias can lead into another zone. Insecurity of the
2184
+ # original QNAME says nothing about that zone's required records.
2185
+ operation = trust_operation
2186
+ operation[:insecure_names] ||= {}
2187
+ question = msg.question.first
2188
+ terminal, = terminal_answer(msg, question)
2189
+ names = [[question.qname, question.qclass], [terminal, question.qclass]]
2190
+ msg.section_rrsets.each do |section, rrsets|
2191
+ next if section == 'additional'
2192
+ rrsets.each { |rrset| names << [rrset.name, rrset.klass] unless rrset.rrs.empty? }
1261
2193
  end
1262
- if ((@verifier_type == VerifierType::DLV) &&
1263
- @added_dlv_key)
1264
- # Remember to check DLV registry as well (if appropriate!)
1265
- # print "Checking DLV for closest anchor\n"
1266
- dlv_anchor = find_closest_dlv_anchor_for(qname)
1267
- # print "Found DLV closest anchor :#{dlv_anchor}\n"
1268
- if (dlv_anchor)
1269
- actual_anchor = follow_chain(dlv_anchor, qname)
1270
- # print "Actual anchor : #{actual_anchor}\n"
1271
- if (actual_anchor)
1272
- # print("DLV Anchor exists for #{qname}, but no signatures in #{msg}\n")
1273
- TheLog.error("DLV Anchor exists for #{qname}, but no signatures in #{msg}")
1274
- msg.security_level = Message::SecurityLevel.BOGUS
1275
- return false
1276
- end
1277
-
1278
- end
2194
+ unless names.uniq.all? { |name, klass| discover_insecure_name(name, klass) }
2195
+ msg.security_level = Message::SecurityLevel.BOGUS
2196
+ return false
1279
2197
  end
1280
- # print "Returning Message insecure OK\n"
1281
2198
  msg.security_level = Message::SecurityLevel.INSECURE
1282
2199
  return true
1283
2200
  end
1284
2201
 
1285
2202
  def validate(msg, query)
2203
+ with_operational_validation(msg) { validate_internal(msg, query) }
2204
+ end
2205
+
2206
+ # Cached-key verification for Dnssec's validation fast path uses the same
2207
+ # algorithm policy and proof-lifetime accounting as chain discovery.
2208
+ def verify_for_validation(msg) # :nodoc:
2209
+ with_operational_validation(msg) { verify_result(msg) }
2210
+ end
2211
+
2212
+ def with_operational_validation(msg)
2213
+ with_validation do
2214
+ operation = trust_operation
2215
+ previous = operation.values_at(:operational, :insecure_rrsets, :insecure_names, :name_security,
2216
+ :insecurity_expirations, :last_insecure_expiration)
2217
+ operation[:operational] = true
2218
+ operation[:insecure_rrsets], operation[:insecure_names] = {}, {}
2219
+ operation[:name_security] = {}
2220
+ operation[:insecurity_expirations] = []
2221
+ begin
2222
+ result = yield
2223
+ if result == :insecure || (result == true && msg.security_level == Message::SecurityLevel.INSECURE)
2224
+ msg.header.ad = false
2225
+ bound_insecure_lifetime(msg, operation[:insecurity_expirations]) if result
2226
+ end
2227
+ result
2228
+ ensure
2229
+ operation[:operational], operation[:insecure_rrsets], operation[:insecure_names], operation[:name_security],
2230
+ operation[:insecurity_expirations], operation[:last_insecure_expiration] = previous
2231
+ end
2232
+ end
2233
+ end
2234
+ private :with_operational_validation
2235
+
2236
+ # An unsigned answer's TTL does not authenticate how long its insecure
2237
+ # classification is valid. Cache that classification only within the
2238
+ # authenticated no-DS/unsupported-DS proof and its complete trust chain.
2239
+ def bound_insecure_lifetime(msg, expirations)
2240
+ previous = msg.instance_variable_get(:@dnsruby_authenticated_expiration)
2241
+ if expirations.empty? || expirations.any?(&:nil?)
2242
+ msg.remove_instance_variable(:@dnsruby_authenticated_expiration) if previous
2243
+ return
2244
+ end
2245
+ signed_expirations = []
2246
+ msg.each_resource do |rr|
2247
+ deadline = rr.instance_variable_get(:@dnsruby_authenticated_expiration)
2248
+ signed_expirations << deadline if deadline
2249
+ end
2250
+ deadline = (expirations + signed_expirations + Array(previous)).min
2251
+ msg.instance_variable_set(:@dnsruby_authenticated_expiration, deadline)
2252
+ end
2253
+ private :bound_insecure_lifetime
2254
+
2255
+ private def validate_internal(msg, query)
2256
+ discard_unsupported_nsec3_proofs(msg)
1286
2257
  if (msg.rrsets('RRSIG').length == 0)
1287
2258
  return validate_no_rrsigs(msg)
1288
2259
  end
@@ -1323,12 +2294,21 @@ module Dnsruby
1323
2294
  if (msg.security_level.code != Message::SecurityLevel::SECURE)
1324
2295
  begin
1325
2296
  # print "Trying to verify one last time\n"
1326
-
1327
- if verify(msg) # Just make sure we haven't picked the keys up anywhere
1328
- msg.security_level = Message::SecurityLevel.SECURE
2297
+ result = begin
2298
+ # Cached target keys may already authenticate the entire answer.
2299
+ # A matching key tag alone cannot establish that during rollover.
2300
+ verify_result(msg)
2301
+ rescue VerifyError
2302
+ raise unless discover_signer_keys(msg)
2303
+ verify_result(msg)
2304
+ end
2305
+ if result
2306
+ set_verified_security_level(msg, result)
1329
2307
  return true
1330
2308
  end
1331
- rescue VerifyError
2309
+ error ||= VerifyError.new('Required DNSSEC records did not validate')
2310
+ rescue VerifyError => verification_error
2311
+ error ||= verification_error
1332
2312
  end
1333
2313
  end
1334
2314
  if (error)
@@ -1344,7 +2324,110 @@ module Dnsruby
1344
2324
  end
1345
2325
  end
1346
2326
 
2327
+ # An alias can cross zone boundaries. Walking only the original QNAME
2328
+ # cannot discover keys for the target's answer or negative proof. These
2329
+ # lookups remain inside the caller's validation budget and trust operation.
2330
+ def discover_signer_keys(msg)
2331
+ discovered = {}
2332
+ attempted = false
2333
+ qname = msg.question.first&.qname
2334
+ msg.section_rrsets.each do |section, rrsets|
2335
+ next if section == 'additional'
2336
+ rrsets.each do |rrset|
2337
+ next if rrset.rrs.empty?
2338
+ if rrset.sigs.empty? || (trust_operation&.fetch(:operational, false) &&
2339
+ rrset.sigs.none? { |sig| algorithm_supported?(sig.algorithm, operational: true) })
2340
+ attempted = true if discover_insecure_rrset(rrset)
2341
+ next
2342
+ end
2343
+ begin
2344
+ signatures = valid_signatures(rrset)
2345
+ rescue VerifyError
2346
+ attempted = true if discover_insecure_rrset(rrset)
2347
+ next # Ordinary verification will report the invalid RRset.
2348
+ end
2349
+ signatures.map(&:signers_name).uniq.each do |zone|
2350
+ ValidationBudget.current&.check!(1)
2351
+ if qname && within_zone?(qname, zone) # The original chain already visited this zone.
2352
+ if msg.security_level == Message::SecurityLevel.INSECURE
2353
+ attempted = true if discover_insecure_rrset(rrset)
2354
+ end
2355
+ next
2356
+ end
2357
+ if discovered.key?(zone.canonical)
2358
+ attempted = true if discovered[zone.canonical] == :insecure && discover_insecure_rrset(rrset)
2359
+ next
2360
+ end
2361
+ discovered[zone.canonical] = :failed
2362
+ anchor = find_closest_anchor_for(zone)
2363
+ anchor ||= find_closest_dlv_anchor_for(zone) if @verifier_type == VerifierType::DLV && @added_dlv_key
2364
+ unless anchor
2365
+ discovered[zone.canonical] = :insecure
2366
+ attempted = true if discover_insecure_rrset(rrset)
2367
+ next
2368
+ end
2369
+ attempted = true
2370
+ begin
2371
+ if follow_chain(anchor, zone)
2372
+ discovered[zone.canonical] = :secure
2373
+ else
2374
+ discovered[zone.canonical] = :insecure
2375
+ # Honor a more-specific configured anchor at the owner, even
2376
+ # when the signature names an unsigned ancestor zone.
2377
+ discover_insecure_rrset(rrset)
2378
+ end
2379
+ rescue VerifyError
2380
+ # An alternative signature may still validate this RRset. A
2381
+ # budget abort deliberately propagates past this fallback.
2382
+ end
2383
+ end
2384
+ end
2385
+ end
2386
+ if trust_operation&.fetch(:insecure_names, {})&.any? && (question = msg.question.first)
2387
+ terminal, answered = terminal_answer(msg, question)
2388
+ discover_insecure_name(terminal, question.qclass) if !answered && terminal != question.qname
2389
+ end
2390
+ attempted
2391
+ end
2392
+ private :discover_signer_keys
2393
+
2394
+ private def discover_insecure_rrset(rrset)
2395
+ return false unless discover_insecure_name(rrset.name, rrset.klass)
2396
+ trust_operation[:insecure_rrsets][[rrset.name.canonical, rrset.klass.code, rrset.type.code]] = true
2397
+ end
2398
+
2399
+ def discover_insecure_name(name, klass)
2400
+ operation = trust_operation
2401
+ return false unless operation && operation[:insecure_names]
2402
+ identity = [name.canonical, klass.code]
2403
+ return true if operation[:insecure_names][identity]
2404
+ classifications = operation[:name_security] ||= {}
2405
+ return classifications[identity] if classifications.key?(identity)
2406
+ ValidationBudget.current&.check!(1)
2407
+ anchors = [find_closest_anchor_for(name)]
2408
+ anchors << find_closest_dlv_anchor_for(name) if @verifier_type == VerifierType::DLV && @added_dlv_key
2409
+ anchors = anchors.select { |anchor| anchor && anchor.klass == klass }.uniq
2410
+ # No applicable anchor is an unanchored result for this verifier. It
2411
+ # must not turn into a reusable authenticated proof of insecurity.
2412
+ operation[:insecurity_expirations]&.push(nil) if anchors.empty?
2413
+ if anchors.any? { |anchor| follow_chain(anchor, name) }
2414
+ classifications[identity] = false
2415
+ return false
2416
+ end
2417
+ operation[:insecure_names][identity] = true
2418
+ classifications[identity] = true
2419
+ rescue ValidationLimitError
2420
+ raise
2421
+ rescue VerifyError
2422
+ false # Missing or invalid delegation evidence remains a validation failure.
2423
+ end
2424
+ private :discover_insecure_name
2425
+
1347
2426
  def try_to_follow_from_anchor(closest_anchor, msg, qname) # :nodoc:
2427
+ with_validation { try_to_follow_from_anchor_internal(closest_anchor, msg, qname) }
2428
+ end
2429
+
2430
+ private def try_to_follow_from_anchor_internal(closest_anchor, msg, qname)
1348
2431
  error = nil
1349
2432
  if (closest_anchor)
1350
2433
  # Then try to descend to the level we're interested in
@@ -1356,7 +2439,7 @@ module Dnsruby
1356
2439
  msg.security_level = Message::SecurityLevel.BOGUS
1357
2440
  return e
1358
2441
  end
1359
- # @TODO@ We need to de ermine whether there was simply no DS record, or whether there was a failure
2442
+ # follow_chain returns false only for an authenticated unsigned delegation.
1360
2443
  if (!actual_anchor)
1361
2444
  TheLog.debug("Unable to follow chain from anchor : #{closest_anchor.name}")
1362
2445
  msg.security_level = Message::SecurityLevel.INSECURE
@@ -1366,9 +2449,9 @@ module Dnsruby
1366
2449
  TheLog.debug("Found anchor #{actual_anchor.name}, #{actual_anchor.type} for #{qname} : #{actual_anchor_keys}")
1367
2450
  # print "Found anchor #{actual_anchor.name}, #{actual_anchor.type} for #{qname} : #{actual_anchor_keys}\n"
1368
2451
  begin
1369
- if (verify(msg, actual_anchor))
2452
+ if (result = verify_result(msg, actual_anchor))
1370
2453
  TheLog.debug("Validated #{qname}")
1371
- msg.security_level = Message::SecurityLevel.SECURE
2454
+ set_verified_security_level(msg, result)
1372
2455
  end
1373
2456
  rescue VerifyError => e
1374
2457
  TheLog.info("BOGUS #{qname}! Error : #{e}")
@@ -1384,5 +2467,12 @@ module Dnsruby
1384
2467
  return error
1385
2468
  end
1386
2469
 
2470
+ def set_verified_security_level(msg, result)
2471
+ msg.security_level = result == :secure ? Message::SecurityLevel.SECURE : Message::SecurityLevel.INSECURE
2472
+ msg.header.ad = false if result == :insecure
2473
+ msg.security_error = nil
2474
+ end
2475
+ private :set_verified_security_level
2476
+
1387
2477
  end
1388
- end
2478
+ end