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
@@ -0,0 +1,615 @@
1
+ require_relative 'tc_key_cache_identity'
2
+
3
+ class DnssecCacheLifetimeTest < Minitest::Test
4
+ def setup
5
+ @now = Time.utc(2026, 11, 13, 12)
6
+ @private_keys = KeyCacheIdentityTest::PRIVATE_KEYS.map { |pem| OpenSSL::PKey::EC.new(pem) }
7
+ @parent, @child = ['cache.invalid.', 'child.cache.invalid.'].each_with_index.map do |name, index|
8
+ bytes = @private_keys[index].public_key.to_octet_string(:uncompressed).byteslice(1, 64)
9
+ Dnsruby::RR.create("#{name} 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(bytes)}")
10
+ end
11
+ @ds = Dnsruby::RR::DS.from_key(@child, 2)
12
+ @ds.ttl = 300
13
+ end
14
+
15
+ def test_learned_dnskeys_use_every_authenticated_ttl_bound_and_only_a_valid_signature
16
+ limits = [{ ttl: 7 }, { signature_ttl: 7 }, { original_ttl: 7 }, { lifetime: 7 }]
17
+ limits.each do |limit|
18
+ [false, true].each do |invalid_first|
19
+ verifier = new_verifier
20
+ verifier.add_trust_anchor(@ds)
21
+ records = signed(@child, @child, 1, **limit)
22
+ valid = records.sigs.first
23
+ invalid = valid.clone
24
+ invalid.expiration = @now.to_i + 7200
25
+ invalid.signature = "\0" * 64
26
+ signatures = invalid_first ? [invalid, valid] : [valid, invalid]
27
+ records = Dnsruby::RRSet.new(records.rrs + signatures)
28
+
29
+ at(0) do
30
+ assert(verifier.verify(records), limit.inspect)
31
+ assert_equal(7, records.ttl, limit.inspect)
32
+ end
33
+ at(6) { assert_cached_child(verifier) }
34
+ at(7) { assert_empty(verifier.get_keys_to_check) }
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_bad_dnskey_signature_cannot_publish_a_ds_matched_key
40
+ [:configured, :explicit, :learned].each do |source|
41
+ verifier = new_verifier
42
+ ds_records = signed(@ds, @parent, 0)
43
+ records = signed(@child, @child, 1)
44
+ records.sigs.first.signature = "\0" * 64
45
+ at(0) do
46
+ case source
47
+ when :configured
48
+ verifier.add_trust_anchor(@ds)
49
+ when :learned
50
+ verifier.add_trust_anchor(@parent)
51
+ assert(verifier.verify(ds_records))
52
+ end
53
+ explicit_ds = source == :explicit ? @ds : nil
54
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(records, explicit_ds) }
55
+ refute_cached_child(verifier)
56
+ refute(verifier.trusted_keys.any? { |key| key.type == Dnsruby::Types.DNSKEY && same_key?(key, @child) })
57
+ end
58
+ end
59
+ end
60
+
61
+ def test_discovered_ds_limits_child_cache_to_its_original_absolute_deadline
62
+ verifier = new_verifier
63
+ verifier.add_trust_anchor(@parent)
64
+ ds_records = signed(@ds, @parent, 0, ttl: 10)
65
+ child_records = signed(@child, @child, 1)
66
+ at(0) { assert(verifier.verify(ds_records)) }
67
+ at(7) do
68
+ assert(verifier.verify(child_records, ds_records))
69
+ assert_cached_child(verifier)
70
+ end
71
+ at(9) { assert_cached_child(verifier) }
72
+ at(10) do
73
+ refute_cached_child(verifier)
74
+ refute_includes(verifier.trusted_keys, @ds)
75
+ end
76
+ end
77
+
78
+ def test_discovered_ds_expiry_survives_pruning_before_explicit_child_verification
79
+ [{ ttl: 5 }, { signature_ttl: 5 }, { original_ttl: 5 }, { lifetime: 5 }].each do |limit|
80
+ verifier = new_verifier
81
+ verifier.add_trust_anchor(@parent)
82
+ ds_records = signed(@ds, @parent, 0, **limit)
83
+ at(0) { assert(verifier.verify(ds_records)) }
84
+ at(4) { assert_includes(verifier.trusted_keys, @ds) }
85
+ at(5) do
86
+ # This public read prunes the DS cache while the chain lookup retains
87
+ # its original RRset. It must not become fresh explicit trust input.
88
+ refute_includes(verifier.trusted_keys, @ds)
89
+ assert_raises(Dnsruby::VerifyError) do
90
+ verifier.verify(signed(@child, @child, 1), ds_records)
91
+ end
92
+ refute_cached_child(verifier)
93
+ end
94
+ end
95
+ end
96
+
97
+ def test_matching_a_message_dnskey_only_supplies_a_candidate_until_its_rrset_verifies
98
+ verifier = new_verifier
99
+ verifier.add_trust_anchor(@parent)
100
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
101
+ message = Dnsruby::Message.new(@child.name, 'DNSKEY')
102
+ message.add_answer(@child)
103
+ at(0) do
104
+ assert(verifier.verify_ds_rrset(ds_records, nil, message))
105
+ refute_cached_child(verifier)
106
+ assert_includes(verifier.trusted_keys, @ds)
107
+ assert(verifier.verify(signed(@child, @child, 1), ds_records))
108
+ assert_cached_child(verifier)
109
+ end
110
+ at(5) do
111
+ refute_cached_child(verifier)
112
+ assert_raises(Dnsruby::VerifyError) do
113
+ verifier.verify(signed(@child, @child, 1), ds_records)
114
+ end
115
+ refute_cached_child(verifier)
116
+ end
117
+ end
118
+
119
+ def test_a_full_message_caches_dnskeys_only_for_the_authenticated_dnskey_lifetime
120
+ verifier = new_verifier
121
+ verifier.add_trust_anchor(@parent)
122
+ message = message_with(signed(@ds, @parent, 0),
123
+ signed(@child, @child, 1, signature_ttl: 7), signed_child_answer)
124
+ at(0) do
125
+ assert(verifier.verify(message))
126
+ assert_cached_child(verifier)
127
+ assert(verifier.verify(signed_child_answer))
128
+ end
129
+ at(6) { assert_cached_child(verifier) }
130
+ at(7) do
131
+ refute_cached_child(verifier)
132
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
133
+ end
134
+ end
135
+
136
+ def test_full_response_ttls_and_cache_honor_every_authenticated_bound
137
+ [{ ttl: 7 }, { signature_ttl: 7 }, { original_ttl: 7 }, { lifetime: 7 }].each do |limit|
138
+ verifier = new_verifier
139
+ verifier.add_trust_anchor(@child)
140
+ record = Dnsruby::RR.create("#{@child.name} 300 IN A 192.0.2.1")
141
+ message = message_with(signed(record, @child, 1, **limit))
142
+ cache = Dnsruby::Cache.new
143
+ at(0) do
144
+ assert(verifier.verify(message), limit.inspect)
145
+ assert_equal([7, 7], message.answer.map(&:ttl), limit.inspect)
146
+ end
147
+ at(2) { cache.add(message) }
148
+ at(4) do
149
+ found = cache.find(@child.name, 'A')
150
+ assert_equal([3, 3], found.answer.map(&:ttl), limit.inspect)
151
+ assert(verifier.verify(found), limit.inspect)
152
+ assert_equal([3, 3], found.answer.map(&:ttl), limit.inspect)
153
+ cache.add(found)
154
+ end
155
+ at(7) { assert_nil(cache.find(@child.name, 'A'), limit.inspect) }
156
+ end
157
+ end
158
+
159
+ def test_cached_dnskey_revalidation_cannot_renew_its_authenticated_lifetime
160
+ verifier = new_verifier
161
+ verifier.add_trust_anchor(@ds)
162
+ message = message_with(signed(@child, @child, 1, signature_ttl: 7))
163
+ cache = Dnsruby::Cache.new
164
+ at(0) do
165
+ assert(verifier.verify(message))
166
+ cache.add(message)
167
+ end
168
+ recipient = new_verifier
169
+ recipient.add_trust_anchor(@ds)
170
+ at(4) do
171
+ cached = cache.find(@child.name, 'DNSKEY')
172
+ assert(recipient.verify(cached))
173
+ assert_cached_child(recipient)
174
+ assert_equal([3, 3], cached.answer.map(&:ttl))
175
+ end
176
+ at(7) do
177
+ refute_cached_child(recipient)
178
+ assert_nil(cache.find(@child.name, 'DNSKEY'))
179
+ end
180
+ end
181
+
182
+ def test_validation_queue_delay_does_not_restart_received_record_ttls
183
+ verifier = new_verifier
184
+ verifier.add_trust_anchor(@ds)
185
+ wire = message_with(signed(@child, @child, 1, signature_ttl: 7)).encode
186
+ message = at(0) { Dnsruby::Message.decode(wire) }
187
+ cache = Dnsruby::Cache.new
188
+ at(4) do
189
+ assert(verifier.verify(message))
190
+ assert_equal([3, 3], message.answer.map(&:ttl))
191
+ cache.add(message)
192
+ assert_equal([3, 3], cache.find(@child.name, 'DNSKEY').answer.map(&:ttl))
193
+ assert_cached_child(verifier)
194
+ end
195
+ at(7) do
196
+ refute_cached_child(verifier)
197
+ assert_nil(cache.find(@child.name, 'DNSKEY'))
198
+ end
199
+ end
200
+
201
+ def test_public_verification_retains_learned_ds_expiration_across_all_verifiers
202
+ [:anchor_verifier, :root_verifier, :dlv_verifier].each do |source|
203
+ with_public_verifiers do |verifiers|
204
+ verifier = Dnsruby::Dnssec.public_send(source)
205
+ verifier.add_trust_anchor(@parent)
206
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
207
+ at(0) { assert(verifier.verify(ds_records), source.to_s) }
208
+ at(4) do
209
+ assert(Dnsruby::Dnssec.verify(signed(@child, @child, 1), ds_records), source.to_s)
210
+ end
211
+ at(5) do
212
+ verifiers.each do |candidate|
213
+ refute_includes(candidate.trusted_keys, @ds)
214
+ refute_cached_child(candidate)
215
+ end
216
+ [ds_records, ds_records.clone, Marshal.load(Marshal.dump(ds_records))].each do |retained|
217
+ assert_raises(Dnsruby::VerifyError, source.to_s) do
218
+ Dnsruby::Dnssec.verify(signed(@child, @child, 1), retained)
219
+ end
220
+ end
221
+ verifiers.each { |candidate| refute_cached_child(candidate) }
222
+ # A newly supplied DS remains explicit trust input.
223
+ assert(Dnsruby::Dnssec.verify(signed(@child, @child, 1), @ds.clone))
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ def test_public_fallback_cannot_reuse_learned_ds_after_its_source_trust_is_revoked
230
+ [:anchor_verifier, :root_verifier, :dlv_verifier].product([:remove, :clear]).each do |source, operation|
231
+ with_public_verifiers do |verifiers|
232
+ verifier = Dnsruby::Dnssec.public_send(source)
233
+ verifier.add_trust_anchor(@parent)
234
+ ds_records = signed(@ds, @parent, 0)
235
+ at(0) { assert(verifier.verify(ds_records)) }
236
+ at(1) do
237
+ if operation == :remove
238
+ verifier.remove_trust_anchor(@parent)
239
+ else
240
+ verifier.clear_trust_anchors
241
+ end
242
+ assert_raises(Dnsruby::VerifyError, "#{source} #{operation}") do
243
+ Dnsruby::Dnssec.verify(signed(@child, @child, 1), ds_records)
244
+ end
245
+ verifiers.each { |candidate| refute_cached_child(candidate) }
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def test_fresh_explicit_trust_replaces_expired_or_revoked_provenance_for_the_same_key
252
+ [:remove, :clear, :expire].product([:ds, :dnskey]).each do |operation, key_type|
253
+ source = new_verifier
254
+ recipient = new_verifier
255
+ ds_records = signed(@ds, @parent, 0)
256
+ at(0) do
257
+ if operation == :expire
258
+ source.add_trust_anchor_with_expiration(@parent, @now.to_i + 5)
259
+ else
260
+ source.add_trust_anchor(@parent)
261
+ end
262
+ assert(source.verify(ds_records))
263
+ assert(recipient.verify(signed(@child, @child, 1), ds_records))
264
+ assert_cached_child(recipient)
265
+ end
266
+ renewed_at = operation == :expire ? 5 : 1
267
+ at(renewed_at) do
268
+ source.remove_trust_anchor(@parent) if operation == :remove
269
+ source.clear_trust_anchors if operation == :clear
270
+ refute_cached_child(recipient)
271
+ explicit = key_type == :ds ? @ds.clone : @child.clone
272
+ assert(recipient.verify(signed(@child, @child, 1, ttl: 7), explicit), "#{operation} #{key_type}")
273
+ assert_cached_child(recipient)
274
+ assert(recipient.verify(signed_child_answer))
275
+ end
276
+ at(renewed_at + 6) do
277
+ assert_cached_child(recipient)
278
+ assert(recipient.verify(signed_child_answer))
279
+ end
280
+ at(renewed_at + 7) do
281
+ refute_cached_child(recipient)
282
+ assert_raises(Dnsruby::VerifyError) { recipient.verify(signed_child_answer) }
283
+ end
284
+ end
285
+ end
286
+
287
+ def test_closest_anchor_ignores_child_trust_revoked_at_its_source_verifier
288
+ source = new_verifier
289
+ recipient = new_verifier
290
+ source.add_trust_anchor(@parent)
291
+ recipient.add_trust_anchor(@parent)
292
+ ds_records = signed(@ds, @parent, 0)
293
+ name = Dnsruby::Name.create('www.child.cache.invalid.')
294
+ at(0) do
295
+ assert(source.verify(ds_records))
296
+ assert(recipient.verify(signed(@child, @child, 1), ds_records))
297
+ assert(same_key?(@child, recipient.find_closest_anchor_for(name)))
298
+ end
299
+ at(1) do
300
+ source.remove_trust_anchor(@parent)
301
+ refute_cached_child(recipient)
302
+ assert(same_key?(@parent, recipient.find_closest_anchor_for(name)))
303
+ end
304
+ end
305
+
306
+ def test_an_explicit_array_can_use_fresh_trust_after_a_revoked_copy_of_the_same_key
307
+ verifier = new_verifier
308
+ verifier.add_trust_anchor(@parent)
309
+ key_records = signed(@child, @child, 1)
310
+ at(0) do
311
+ assert(verifier.verify(signed(@ds, @parent, 0)))
312
+ assert(verifier.verify(key_records))
313
+ end
314
+ at(1) do
315
+ verifier.remove_trust_anchor(@parent)
316
+ learned, fresh = key_records.rrs.first, @child.clone
317
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer, [learned]) }
318
+ [[learned, fresh], [fresh, learned]].each do |keys|
319
+ assert(verifier.verify(signed_child_answer, keys))
320
+ end
321
+ end
322
+ end
323
+
324
+ def test_zero_ttl_records_remain_usable_through_one_full_message_only
325
+ [[0, 300], [300, 0], [0, 0]].each do |ds_ttl, key_ttl|
326
+ verifier = new_verifier
327
+ verifier.add_trust_anchor(@parent)
328
+ message = message_with(signed(@ds, @parent, 0, ttl: ds_ttl),
329
+ signed(@child, @child, 1, ttl: key_ttl), signed_child_answer)
330
+ at(0) do
331
+ assert(verifier.verify(message), "DS TTL #{ds_ttl}, DNSKEY TTL #{key_ttl}")
332
+ refute_cached_child(verifier)
333
+ # Even at the same clock time, a separate operation cannot use a
334
+ # zero-TTL key learned by the completed message verification.
335
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
336
+ end
337
+ at(1) do
338
+ refute_cached_child(verifier)
339
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
340
+ end
341
+ end
342
+ end
343
+
344
+ def test_zero_ttl_chain_records_remain_usable_through_one_validation_only
345
+ [[0, 300], [300, 0], [0, 0]].each do |ds_ttl, key_ttl|
346
+ verifier = new_verifier
347
+ verifier.add_trust_anchor(@parent)
348
+ ds_records = signed(@ds, @parent, 0, ttl: ds_ttl)
349
+ key_records = signed(@child, @child, 1, ttl: key_ttl)
350
+ message = message_with(signed_child_answer)
351
+ elapsed = 0
352
+ before_response = lambda do |name, type|
353
+ elapsed = 2 if name == @child.name && type == Dnsruby::Types.DNSKEY
354
+ end
355
+ Time.stub(:now, -> { @now + elapsed }) do
356
+ with_chain_resolver(verifier, ds_records, key_records, before_response) do |queries|
357
+ assert(verifier.validate(message, Dnsruby::Message.new(@child.name, 'A')))
358
+ assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
359
+ assert_equal(['DNSKEY', 'DS', 'DNSKEY'], queries.map(&:last))
360
+ end
361
+ refute_cached_child(verifier)
362
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
363
+ end
364
+ end
365
+ end
366
+
367
+ def test_follow_chain_accepts_zero_ttl_ds_without_learning_a_reusable_key
368
+ verifier = new_verifier
369
+ verifier.add_trust_anchor(@parent)
370
+ ds_records = signed(@ds, @parent, 0, ttl: 0)
371
+ at(0) do
372
+ with_chain_resolver(verifier, ds_records, signed(@child, @child, 1)) do
373
+ keys = verifier.follow_chain(@parent, @child.name)
374
+ assert_equal(1, keys.rrs.length)
375
+ assert(same_key?(@child, keys.rrs.first))
376
+ end
377
+ refute_cached_child(verifier)
378
+ refute_includes(verifier.trusted_keys, @ds)
379
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
380
+ end
381
+ end
382
+
383
+ def test_current_chain_can_finish_after_its_ds_ttl_without_extending_the_cache_deadline
384
+ [4, 6].each do |lookup_delay|
385
+ verifier = new_verifier
386
+ verifier.add_trust_anchor(@parent)
387
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
388
+ elapsed = 0
389
+ before_response = lambda do |name, type|
390
+ elapsed = lookup_delay if name == @child.name && type == Dnsruby::Types.DNSKEY
391
+ end
392
+ Time.stub(:now, -> { @now + elapsed }) do
393
+ with_chain_resolver(verifier, ds_records, signed(@child, @child, 1), before_response) do
394
+ keys = verifier.follow_chain(@parent, @child.name)
395
+ assert_equal(1, keys.rrs.length)
396
+ assert(same_key?(@child, keys.rrs.first))
397
+ if lookup_delay < 5
398
+ assert_cached_child(verifier)
399
+ else
400
+ refute_cached_child(verifier)
401
+ end
402
+ end
403
+ elapsed = [elapsed, 5].max
404
+ refute_cached_child(verifier)
405
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
406
+ end
407
+ end
408
+ end
409
+
410
+ def test_a_ds_signature_expiring_during_chain_discovery_cannot_finish_validation
411
+ verifier = new_verifier
412
+ verifier.add_trust_anchor(@parent)
413
+ elapsed = 0
414
+ before_response = lambda do |name, type|
415
+ elapsed = 6 if name == @child.name && type == Dnsruby::Types.DNSKEY
416
+ end
417
+ Time.stub(:now, -> { @now + elapsed }) do
418
+ with_chain_resolver(verifier, signed(@ds, @parent, 0, lifetime: 5),
419
+ signed(@child, @child, 1), before_response) do
420
+ assert_raises(Dnsruby::VerifyError) { verifier.follow_chain(@parent, @child.name) }
421
+ end
422
+ refute_cached_child(verifier)
423
+ end
424
+ end
425
+
426
+ def test_learned_ds_and_dnskey_rrsets_remain_marshal_and_clone_compatible
427
+ verifier = new_verifier
428
+ verifier.add_trust_anchor(@parent)
429
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
430
+ key_records = signed(@child, @child, 1)
431
+ at(0) do
432
+ assert(verifier.verify(ds_records))
433
+ assert(verifier.verify(key_records, ds_records))
434
+ [ds_records, key_records].each do |records|
435
+ [records.clone, Marshal.load(Marshal.dump(records))].each do |copy|
436
+ assert_equal(records.rrs.map(&:to_s), copy.rrs.map(&:to_s))
437
+ assert_equal(records.sigs.map(&:to_s), copy.sigs.map(&:to_s))
438
+ end
439
+ end
440
+ end
441
+ end
442
+
443
+ def test_cloned_learned_records_preserve_expiration_and_revocation
444
+ [:ds, :dnskey].product([:expire, :revoke]).each do |key_type, reason|
445
+ source = new_verifier
446
+ source.add_trust_anchor(@parent)
447
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
448
+ key_records = signed(@child, @child, 1)
449
+ at(0) do
450
+ assert(source.verify(ds_records))
451
+ assert(source.verify(key_records, ds_records))
452
+ end
453
+ learned = (key_type == :ds ? ds_records : key_records).rrs.first
454
+ container = Dnsruby::Message.new(learned.name, learned.type)
455
+ container.add_answer(learned)
456
+ copies = at(3) do
457
+ [learned.clone, container.clone.answer.first, Dnsruby::RRSet.new([learned.clone])]
458
+ end
459
+ recipients = copies.map { new_verifier }
460
+ at(4) do
461
+ copies.zip(recipients).each do |copy, recipient|
462
+ assert(recipient.verify(signed(@child, @child, 1), copy), "#{key_type} #{reason}")
463
+ assert_cached_child(recipient)
464
+ end
465
+ end
466
+ at(reason == :expire ? 5 : 4) do
467
+ source.remove_trust_anchor(@parent) if reason == :revoke
468
+ copies.zip(recipients).each do |copy, recipient|
469
+ refute_cached_child(recipient)
470
+ assert_raises(Dnsruby::VerifyError, "#{key_type} #{reason}") do
471
+ recipient.verify(signed(@child, @child, 1), copy)
472
+ end
473
+ # A separately supplied original remains explicit trust input.
474
+ fresh = (key_type == :ds ? @ds : @child).clone
475
+ assert(recipient.verify(signed(@child, @child, 1), fresh))
476
+ end
477
+ end
478
+ end
479
+ end
480
+
481
+ def test_learning_ds_and_dnskey_records_preserves_equality_and_hash_lookup
482
+ verifier = new_verifier
483
+ verifier.add_trust_anchor(@parent)
484
+ ds_records = signed(@ds, @parent, 0)
485
+ key_records = signed(@child, @child, 1)
486
+ # Populate crypto memoization before taking identical clean snapshots.
487
+ key_records.rrs.each { |key| key.public_key; key.key_tag }
488
+ records = ds_records.rrs + key_records.rrs
489
+ clean = records.map(&:dup)
490
+ lookup = clean.to_h { |record| [record, :known] }
491
+ at(0) do
492
+ assert(verifier.verify(ds_records))
493
+ assert(verifier.verify(key_records, ds_records))
494
+ clean.zip(records).each do |before, learned|
495
+ assert_equal(before, learned)
496
+ assert_equal(before.hash, learned.hash)
497
+ assert_equal(:known, lookup[learned])
498
+ end
499
+ end
500
+ end
501
+
502
+ def test_closest_anchor_lookup_returns_unexpired_learned_ds_rrsets
503
+ verifier = new_verifier
504
+ verifier.add_trust_anchor(@parent)
505
+ ds_records = signed(@ds, @parent, 0, ttl: 5)
506
+ name = Dnsruby::Name.create('www.child.cache.invalid.')
507
+ at(0) { assert(verifier.verify(ds_records)) }
508
+ at(4) { assert_same(ds_records, verifier.find_closest_anchor_for(name)) }
509
+ at(5) { assert_equal(@parent, verifier.find_closest_anchor_for(name)) }
510
+ end
511
+
512
+ def test_explicit_dnskey_anchor_ttl_zero_remains_usable
513
+ verifier = new_verifier
514
+ @parent.ttl = 0
515
+ verifier.add_trust_anchor(@parent)
516
+ answer = Dnsruby::RR.create('www.cache.invalid. 300 IN A 192.0.2.1')
517
+ records = signed(answer, @parent, 0)
518
+ at(100) do
519
+ assert_equal([@parent], verifier.trust_anchors)
520
+ assert(verifier.verify(records))
521
+ end
522
+ end
523
+
524
+ def test_configured_anchor_expiration_invalidates_descendants_at_the_exact_deadline
525
+ verifier = new_verifier
526
+ verifier.add_trust_anchor_with_expiration(@parent, @now.to_i + 5)
527
+ ds_records = signed(@ds, @parent, 0)
528
+ at(0) do
529
+ assert(verifier.verify(ds_records))
530
+ assert(verifier.verify(signed(@child, @child, 1)))
531
+ end
532
+ at(5) do
533
+ assert_empty(verifier.get_keys_to_check)
534
+ assert_empty(verifier.trusted_keys)
535
+ assert_empty(verifier.trust_anchors)
536
+ assert_raises(Dnsruby::VerifyError) { verifier.verify(signed(@child, @child, 1)) }
537
+ end
538
+ end
539
+
540
+ private
541
+
542
+ def same_key?(first, second)
543
+ Dnsruby::KeyCache.key_identity(first) == Dnsruby::KeyCache.key_identity(second)
544
+ end
545
+
546
+ def assert_cached_child(verifier)
547
+ assert(verifier.get_keys_to_check.any? { |key| same_key?(key, @child) }, 'Expected a cached child DNSKEY')
548
+ end
549
+
550
+ def refute_cached_child(verifier)
551
+ refute(verifier.get_keys_to_check.any? { |key| same_key?(key, @child) }, 'Unexpected cached child DNSKEY')
552
+ end
553
+
554
+ def new_verifier
555
+ Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
556
+ end
557
+
558
+ def with_public_verifiers
559
+ Dnsruby::Dnssec.reset
560
+ yield [Dnsruby::Dnssec.anchor_verifier, Dnsruby::Dnssec.root_verifier, Dnsruby::Dnssec.dlv_verifier]
561
+ ensure
562
+ Dnsruby::Dnssec.reset
563
+ end
564
+
565
+ def signed_child_answer
566
+ signed(Dnsruby::RR.create("#{@child.name} 300 IN A 192.0.2.1"), @child, 1)
567
+ end
568
+
569
+ def message_with(*rrsets)
570
+ message = Dnsruby::Message.new(rrsets.last.name, rrsets.last.type)
571
+ rrsets.each { |rrset| rrset.each { |rr| message.add_answer(rr) } }
572
+ message
573
+ end
574
+
575
+ def with_chain_resolver(verifier, ds_records, key_records, before_response = nil)
576
+ responses = {
577
+ [@parent.name.canonical.to_s, 'DNSKEY'] => message_with(signed(@parent, @parent, 0)),
578
+ [@child.name.canonical.to_s, 'DS'] => message_with(ds_records),
579
+ [@child.name.canonical.to_s, 'DNSKEY'] => message_with(key_records)
580
+ }
581
+ queries = []
582
+ resolver = Object.new
583
+ resolver.define_singleton_method(:query_no_validation_or_recursion) do |name, type|
584
+ queries << [name.canonical.to_s, type.to_s]
585
+ before_response&.call(name, type)
586
+ responses.fetch(queries.last)
587
+ end
588
+ Dnsruby::Dnssec.stub(:do_validation_with_recursor?, false) do
589
+ Dnsruby::Dnssec.stub(:default_resolver, resolver) do
590
+ verifier.stub(:get_nameservers_for, resolver) do
591
+ verifier.stub(:get_nameservers_from_message, resolver) { yield queries }
592
+ end
593
+ end
594
+ end
595
+ end
596
+
597
+ def at(seconds, &block)
598
+ Time.stub(:now, @now + seconds, &block)
599
+ end
600
+
601
+ def signed(record, key, signer, ttl: 300, signature_ttl: 300, original_ttl: 300, lifetime: 3600)
602
+ record = record.clone
603
+ record.ttl = ttl
604
+ signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: signature_ttl,
605
+ type_covered: record.type, algorithm: 13, labels: record.name.labels.length,
606
+ original_ttl: original_ttl, expiration: @now.to_i + lifetime, inception: @now.to_i - 60,
607
+ key_tag: key.key_tag, signers_name: key.name)
608
+ canonical_record = record.clone
609
+ canonical_record.ttl = original_ttl
610
+ data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(canonical_record, true) }.to_s
611
+ der = @private_keys[signer].dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + data))
612
+ signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
613
+ Dnsruby::RRSet.new([record, signature])
614
+ end
615
+ end