dnsruby 1.74.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
require_relative 'tc_key_cache_identity'
|
|
2
|
+
require_relative 'tc_pipeline_send'
|
|
3
|
+
require_relative 'tc_resolver_validation'
|
|
4
|
+
|
|
5
|
+
class KeyCacheIdentityTest
|
|
6
|
+
def test_expiration_alone_revokes_learned_keys_and_preserves_other_anchors
|
|
7
|
+
clock = @now
|
|
8
|
+
Time.stub(:now, -> { clock }) do
|
|
9
|
+
@verifier.add_trust_anchor_with_expiration(@a, @now.to_i + 1)
|
|
10
|
+
@verifier.add_trust_anchor_with_expiration(@b, @now.to_i + 120)
|
|
11
|
+
assert(@verifier.verify(signed(@a, @a, 0)))
|
|
12
|
+
clock = @now + 2
|
|
13
|
+
# Do not read trust_anchors first: verification must notice expiry itself.
|
|
14
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(signed(@record, @a, 0)) }
|
|
15
|
+
assert_empty(@verifier.trusted_keys)
|
|
16
|
+
assert(@verifier.verify(signed(@record, @b, 1)))
|
|
17
|
+
assert_equal([@b], @verifier.trust_anchors)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_expiration_during_verification_blocks_publication_without_an_anchor_read
|
|
22
|
+
@verifier.add_trust_anchor_with_expiration(@a, @now.to_i + 1)
|
|
23
|
+
clock = @now
|
|
24
|
+
Time.stub(:now, -> { clock }) do
|
|
25
|
+
pause_after_verification(-> { clock = @now + 2 }) do
|
|
26
|
+
assert(@verifier.verify(signed(@a, @a, 0)))
|
|
27
|
+
end
|
|
28
|
+
assert_empty(@verifier.instance_variable_get(:@trusted_keys).keys)
|
|
29
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(signed(@record, @a, 0)) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_matching_signatures_reuse_canonical_rdata_and_try_different_metadata
|
|
34
|
+
valid = signed(@record, @a, 0).sigs.first
|
|
35
|
+
invalid = 20.times.map do |i|
|
|
36
|
+
valid.clone.tap do |sig|
|
|
37
|
+
sig.original_ttl = 500 if i.odd?
|
|
38
|
+
sig.signature = [i].pack('n') + "\0" * 62
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
records = Dnsruby::RRSet.new([@record] + invalid + [valid])
|
|
42
|
+
record = records.rrs.first
|
|
43
|
+
encode = record.method(:encode_rdata)
|
|
44
|
+
encodings = 0
|
|
45
|
+
record.define_singleton_method(:encode_rdata) do |*args|
|
|
46
|
+
encodings += 1
|
|
47
|
+
encode.call(*args)
|
|
48
|
+
end
|
|
49
|
+
# Encoding reuse should not depend on verification crossing a TTL second.
|
|
50
|
+
Time.stub(:now, @now) { assert(@verifier.verify_rrset(records, [@b, @a])) }
|
|
51
|
+
assert_equal(1, encodings)
|
|
52
|
+
assert_equal(300, record.ttl)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_wildcard_signature_and_invalid_label_count
|
|
56
|
+
wildcard = Dnsruby::RR.create('*.collision.invalid. 300 IN A 192.0.2.1')
|
|
57
|
+
signature = signed(wildcard, @a, 0).sigs.first
|
|
58
|
+
signature.labels = 2
|
|
59
|
+
bytes = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(wildcard, true) }.to_s
|
|
60
|
+
der = @private_keys.first.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + bytes))
|
|
61
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
62
|
+
signature.name = @record.name
|
|
63
|
+
records = Dnsruby::RRSet.new([@record, signature])
|
|
64
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
65
|
+
records.sigs.first.labels = 4
|
|
66
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, @a) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_signature_limit_is_shared_across_nested_operations_and_can_be_configured
|
|
70
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 2)
|
|
71
|
+
records = signed(@record, @a, 0)
|
|
72
|
+
budget = Dnsruby::ValidationBudget.new(limits: limits)
|
|
73
|
+
error = assert_raises(Dnsruby::ValidationLimitError) do
|
|
74
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
75
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
76
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
77
|
+
begin
|
|
78
|
+
@verifier.verify_rrset(records, @a)
|
|
79
|
+
rescue Dnsruby::VerifyError
|
|
80
|
+
flunk('A nested limit must bypass ordinary verification fallbacks')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
85
|
+
assert_equal(error.backtrace, error.cause.backtrace)
|
|
86
|
+
assert_raises(Dnsruby::ValidationLimitError) { budget.check! }
|
|
87
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
88
|
+
Dnsruby::ValidationBudget.with(budget) { flunk('An exhausted budget was reused') }
|
|
89
|
+
end
|
|
90
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_direct_verifier_limit_stops_colliding_key_fallback
|
|
94
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 1)
|
|
95
|
+
records = signed(@record, @a, 0)
|
|
96
|
+
Dnsruby::Dnssec.stub(:validation_limits, limits) do
|
|
97
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
98
|
+
@verifier.verify_rrset(records, [@b, @a])
|
|
99
|
+
end
|
|
100
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
101
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_dnssec_rrset_entry_point_keeps_the_verifiers_limit
|
|
106
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 1)
|
|
107
|
+
records = signed(@record, @a, 0)
|
|
108
|
+
Dnsruby::Dnssec.stub(:validation_limits, limits) do
|
|
109
|
+
Dnsruby::Dnssec.anchor_verifier.stub(:verify_rrset, @verifier.method(:verify_rrset)) do
|
|
110
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
111
|
+
Dnsruby::Dnssec.verify_rrset(records, [@b, @a])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
116
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_direct_verifier_checks_cancellation_after_an_early_success_return
|
|
120
|
+
records = signed(@record, @a, 0)
|
|
121
|
+
verify = ->(*) { Dnsruby::ValidationBudget.current.cancel; true }
|
|
122
|
+
@verifier.stub(:verify_rrset_with_key, verify) do
|
|
123
|
+
assert_raises(Dnsruby::ValidationCancelled) { @verifier.verify_rrset(records, @a) }
|
|
124
|
+
end
|
|
125
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_discovery_exhaustion_does_not_become_an_insecure_delegation
|
|
129
|
+
@verifier.add_trust_anchor(@a)
|
|
130
|
+
key_response = Dnsruby::Message.new(@a.name, 'DNSKEY')
|
|
131
|
+
signed(@a, @a, 0).each { |rr| key_response.add_answer(rr) }
|
|
132
|
+
message = Dnsruby::Message.new(@record.name, 'A')
|
|
133
|
+
queries = []
|
|
134
|
+
resolver = Object.new
|
|
135
|
+
resolver.define_singleton_method(:query_no_validation_or_recursion) do |name, type|
|
|
136
|
+
queries << [name, type]
|
|
137
|
+
if type == Dnsruby::Types.DS
|
|
138
|
+
Dnsruby::ValidationBudget.current.check!(100_001)
|
|
139
|
+
end
|
|
140
|
+
key_response
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
Dnsruby::Dnssec.stub(:do_validation_with_recursor?, false) do
|
|
144
|
+
Dnsruby::Dnssec.stub(:default_resolver, resolver) do
|
|
145
|
+
@verifier.stub(:get_nameservers_for, resolver) do
|
|
146
|
+
@verifier.stub(:get_nameservers_from_message, resolver) do
|
|
147
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
148
|
+
@verifier.try_to_follow_from_anchor(@a, message, @record.name)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
assert_equal([Dnsruby::Types.DNSKEY, Dnsruby::Types.DS], queries.map(&:last))
|
|
155
|
+
refute_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
|
|
156
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_work_limit_bounds_nsec3_hash_iterations
|
|
160
|
+
budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 100))
|
|
161
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
162
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
163
|
+
Dnsruby::RR::NSEC3.calculate_hash('example.', 1000, ''.b, Dnsruby::Nsec3HashAlgorithms.SHA_1)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_cancellation_after_crypto_prevents_success_or_learned_key_publication
|
|
169
|
+
records = signed(@a, @a, 0)
|
|
170
|
+
@verifier.add_trust_anchor(@a)
|
|
171
|
+
budget = Dnsruby::ValidationBudget.new
|
|
172
|
+
key = @a.public_key
|
|
173
|
+
verify = key.method(:dsa_verify_asn1)
|
|
174
|
+
key.define_singleton_method(:dsa_verify_asn1) do |*args|
|
|
175
|
+
result = verify.call(*args)
|
|
176
|
+
budget.cancel
|
|
177
|
+
result
|
|
178
|
+
end
|
|
179
|
+
assert_raises(Dnsruby::ValidationCancelled) do
|
|
180
|
+
Dnsruby::ValidationBudget.with(budget) { @verifier.verify(records) }
|
|
181
|
+
end
|
|
182
|
+
assert_empty(@verifier.trusted_keys)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class DnssecBudgetTest < Minitest::Test
|
|
187
|
+
def test_lookup_stops_cross_validation_fallbacks_and_become_public_errors
|
|
188
|
+
[Dnsruby::QueryControl::Cancelled, Dnsruby::QueryControl::DeadlineExceeded].each do |stop_class|
|
|
189
|
+
public_class = stop_class == Dnsruby::QueryControl::Cancelled ?
|
|
190
|
+
Dnsruby::ValidationCancelled : Dnsruby::ValidationLimitError
|
|
191
|
+
error = assert_raises(public_class) do
|
|
192
|
+
Dnsruby::ValidationBudget.with do |budget|
|
|
193
|
+
assert_same(budget.control, Dnsruby::QueryControl.current)
|
|
194
|
+
begin
|
|
195
|
+
Dnsruby::ValidationBudget.with do
|
|
196
|
+
if stop_class == Dnsruby::QueryControl::Cancelled
|
|
197
|
+
budget.control.cancel
|
|
198
|
+
Dnsruby::QueryControl.current.check!
|
|
199
|
+
else
|
|
200
|
+
Dnsruby::TransportClock.stub(:now, budget.deadline) do
|
|
201
|
+
Dnsruby::QueryControl.current.check!
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
rescue Dnsruby::VerifyError, Dnsruby::ResolvError
|
|
206
|
+
flunk('A stopped lookup must bypass validation and lookup fallbacks')
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
assert_instance_of(stop_class, error.cause)
|
|
211
|
+
assert_equal(error.cause.backtrace, error.backtrace)
|
|
212
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
213
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_validation_obeys_and_restores_an_enclosing_lookup_control
|
|
218
|
+
parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 1)
|
|
219
|
+
assert_raises(Dnsruby::QueryControl::Cancelled) do
|
|
220
|
+
Dnsruby::QueryControl.with(parent) do
|
|
221
|
+
error = assert_raises(Dnsruby::ValidationCancelled) do
|
|
222
|
+
Dnsruby::ValidationBudget.with do |budget|
|
|
223
|
+
assert_equal(parent.deadline, budget.deadline)
|
|
224
|
+
parent.cancel
|
|
225
|
+
budget.check!
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
assert_instance_of(Dnsruby::QueryControl::Cancelled, error.cause)
|
|
229
|
+
assert_same(parent, Dnsruby::QueryControl.current)
|
|
230
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_lookup_deadline_limits_validation_even_when_its_own_timeout_is_longer
|
|
237
|
+
parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 1)
|
|
238
|
+
Dnsruby::QueryControl.with(parent) do
|
|
239
|
+
error = assert_raises(Dnsruby::ValidationLimitError) do
|
|
240
|
+
Dnsruby::ValidationBudget.with do |budget|
|
|
241
|
+
Dnsruby::TransportClock.stub(:now, parent.deadline) { budget.check! }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
assert_instance_of(Dnsruby::QueryControl::DeadlineExceeded, error.cause)
|
|
245
|
+
assert_same(parent, Dnsruby::QueryControl.current)
|
|
246
|
+
end
|
|
247
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_lookup_waits_do_not_consume_dnssec_work_allowance
|
|
251
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_work: 1)
|
|
252
|
+
budget = Dnsruby::ValidationBudget.new(limits: limits)
|
|
253
|
+
replies = Queue.new
|
|
254
|
+
3.times { replies << :reply }
|
|
255
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
256
|
+
budget.check!(1)
|
|
257
|
+
3.times { assert_equal(:reply, Dnsruby::QueryControl.current.pop(replies)) }
|
|
258
|
+
end
|
|
259
|
+
assert_raises(Dnsruby::ValidationLimitError) { budget.check!(1) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_exhaustion_stays_terminal_when_a_callback_swallows_the_abort
|
|
263
|
+
budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 1))
|
|
264
|
+
rescued = false
|
|
265
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
266
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
267
|
+
begin
|
|
268
|
+
budget.check!(2)
|
|
269
|
+
rescue StandardError
|
|
270
|
+
rescued = true
|
|
271
|
+
end
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
assert(rescued)
|
|
276
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def test_direct_validation_callback_does_not_swallow_cancellation
|
|
280
|
+
level = Dnsruby::Message::SecurityLevel.BOGUS
|
|
281
|
+
callback = ->(*) do
|
|
282
|
+
Dnsruby::ValidationBudget.with do |budget|
|
|
283
|
+
budget.cancel
|
|
284
|
+
budget.check!
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
assert_raises(Dnsruby::ValidationCancelled) do
|
|
288
|
+
Dnsruby::Dnssec.try_validation(level, '', level, callback, Dnsruby::Message.new, nil)
|
|
289
|
+
end
|
|
290
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def test_anchor_fallbacks_share_limits_and_stop_at_exhaustion
|
|
294
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 1)
|
|
295
|
+
calls = []
|
|
296
|
+
anchor = ->(*) do
|
|
297
|
+
calls << :anchor
|
|
298
|
+
Dnsruby::ValidationBudget.current.signature!
|
|
299
|
+
raise Dnsruby::VerifyError, 'Try another anchor'
|
|
300
|
+
end
|
|
301
|
+
root = ->(*) do
|
|
302
|
+
calls << :root
|
|
303
|
+
Dnsruby::ValidationBudget.current.signature!
|
|
304
|
+
raise Dnsruby::VerifyError, 'Try DLV'
|
|
305
|
+
end
|
|
306
|
+
dlv = ->(*) { flunk('Exhaustion must not try the DLV fallback') }
|
|
307
|
+
Dnsruby::Dnssec.stub(:validation_limits, limits) do
|
|
308
|
+
Dnsruby::Dnssec.anchor_verifier.stub(:verify_result, anchor) do
|
|
309
|
+
Dnsruby::Dnssec.root_verifier.stub(:verify_result, root) do
|
|
310
|
+
Dnsruby::Dnssec.dlv_verifier.stub(:verify_result, dlv) do
|
|
311
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
312
|
+
Dnsruby::Dnssec.verify(Dnsruby::Message.new('example.', 'A'))
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
assert_equal([:anchor, :root], calls)
|
|
319
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_worker_delivers_one_public_terminal_error_and_unregisters
|
|
323
|
+
[Dnsruby::ValidationLimitError, Dnsruby::ValidationCancelled].each do |error_class|
|
|
324
|
+
completions, registrations, removals = [], [], []
|
|
325
|
+
selector = Object.new
|
|
326
|
+
selector.define_singleton_method(:register_validation) { |*args| registrations << args; true }
|
|
327
|
+
selector.define_singleton_method(:unregister_validation) { |*args| removals << args }
|
|
328
|
+
selector.define_singleton_method(:push_validation_response_to_select) { |*args| completions << args }
|
|
329
|
+
resolver = Object.new
|
|
330
|
+
resolver.define_singleton_method(:dnssec) { true }
|
|
331
|
+
query = Dnsruby::Message.new('example.', 'A')
|
|
332
|
+
query.do_validation = true
|
|
333
|
+
response = query.clone
|
|
334
|
+
response.header.qr = true
|
|
335
|
+
validator = Dnsruby::ValidatorThread.new(:client, Queue.new, response, nil, query, selector, resolver)
|
|
336
|
+
validate = ->(*) do
|
|
337
|
+
budget = Dnsruby::ValidationBudget.current
|
|
338
|
+
budget.cancel if error_class == Dnsruby::ValidationCancelled
|
|
339
|
+
budget.check!(100_001)
|
|
340
|
+
end
|
|
341
|
+
Dnsruby::Dnssec.stub(:validate_with_query, validate) do
|
|
342
|
+
Dnsruby::PacketSender.stub(:cache, ->(*) { flunk('An aborted response was cached') }) do
|
|
343
|
+
validator.do_validate
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
assert_equal(1, completions.length)
|
|
347
|
+
assert_instance_of(error_class, completions.first[3])
|
|
348
|
+
assert_equal(Dnsruby::Message::SecurityLevel.BOGUS, response.security_level)
|
|
349
|
+
assert_equal(1, registrations.length)
|
|
350
|
+
assert_instance_of(Dnsruby::QueryControl, registrations.first.last)
|
|
351
|
+
assert_equal(registrations, removals)
|
|
352
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
353
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def test_deadline_and_configuration_validation
|
|
358
|
+
budget = Dnsruby::ValidationBudget.new(deadline: Dnsruby::TransportClock.now - 1)
|
|
359
|
+
assert_raises(Dnsruby::ValidationLimitError) { budget.check! }
|
|
360
|
+
budget = Dnsruby::ValidationBudget.new
|
|
361
|
+
budget.cancel
|
|
362
|
+
assert_raises(Dnsruby::ValidationCancelled) { budget.check! }
|
|
363
|
+
[:timeout, :max_work, :max_signature_verifications].each do |option|
|
|
364
|
+
[0, -1, nil, '100'].each do |bad|
|
|
365
|
+
assert_raises(ArgumentError) { Dnsruby::Dnssec.validation_limits = { option => bad } }
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
assert_raises(ArgumentError) { Dnsruby::Dnssec.validation_limits = { unknown: 3 } }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def test_async_validation_admission_bounds_workers_and_queue
|
|
372
|
+
code = <<~'RUBY'
|
|
373
|
+
require 'dnsruby'
|
|
374
|
+
require 'timeout'
|
|
375
|
+
entered, release = Queue.new, Queue.new
|
|
376
|
+
job = Object.new
|
|
377
|
+
job.define_singleton_method(:do_validate) { entered << true; release.pop }
|
|
378
|
+
4.times { raise 'worker rejected' unless Dnsruby::ValidatorThread.enqueue(job) }
|
|
379
|
+
Timeout.timeout(3) { 4.times { entered.pop } }
|
|
380
|
+
64.times { raise 'queue rejected' unless Dnsruby::ValidatorThread.enqueue(job) }
|
|
381
|
+
raise 'unbounded admission' if Dnsruby::ValidatorThread.enqueue(job)
|
|
382
|
+
raise 'too many workers' unless entered.empty?
|
|
383
|
+
68.times { release << true }
|
|
384
|
+
Timeout.timeout(3) { 64.times { entered.pop } }
|
|
385
|
+
RUBY
|
|
386
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', code)
|
|
387
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
388
|
+
assert_empty(stderr)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_validation_pool_recovers_after_fork_and_worker_exit
|
|
392
|
+
skip 'fork unavailable' unless Process.respond_to?(:fork)
|
|
393
|
+
code = <<~'RUBY'
|
|
394
|
+
require 'dnsruby'
|
|
395
|
+
require 'timeout'
|
|
396
|
+
completed = Queue.new
|
|
397
|
+
job = Object.new
|
|
398
|
+
job.define_singleton_method(:do_validate) { completed << true }
|
|
399
|
+
raise 'initial admission failed' unless Dnsruby::ValidatorThread.enqueue(job)
|
|
400
|
+
Timeout.timeout(3) { completed.pop }
|
|
401
|
+
child_status = ForkTestHelper.run do
|
|
402
|
+
raise 'child rejected' unless Dnsruby::ValidatorThread.enqueue(job)
|
|
403
|
+
Timeout.timeout(3) { completed.pop }
|
|
404
|
+
exit! 0
|
|
405
|
+
end
|
|
406
|
+
raise 'child worker did not run' unless child_status.success?
|
|
407
|
+
# Emulate external termination of otherwise idle workers.
|
|
408
|
+
Dnsruby::ValidatorThread.instance_variable_get(:@workers).each { |worker| worker.kill; worker.join }
|
|
409
|
+
raise 'replacement rejected' unless Dnsruby::ValidatorThread.enqueue(job)
|
|
410
|
+
Timeout.timeout(3) { completed.pop }
|
|
411
|
+
RUBY
|
|
412
|
+
source = "require #{File.expand_path('fork_test_helper', __dir__).inspect}\n#{code}"
|
|
413
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', source)
|
|
414
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
415
|
+
assert_empty(stderr)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def test_partial_worker_creation_failure_keeps_pool_bounded_and_recoverable
|
|
419
|
+
code = <<~'RUBY'
|
|
420
|
+
require 'dnsruby'
|
|
421
|
+
require 'timeout'
|
|
422
|
+
original = Thread.method(:new)
|
|
423
|
+
Thread.define_singleton_method(:new) { |*| raise ThreadError, 'no worker available' }
|
|
424
|
+
raise 'work accepted with no workers' if Dnsruby::ValidatorThread.enqueue(Object.new)
|
|
425
|
+
started = 0
|
|
426
|
+
Thread.define_singleton_method(:new) do |*args, &block|
|
|
427
|
+
raise ThreadError, 'worker unavailable' if started == 1
|
|
428
|
+
started += 1
|
|
429
|
+
original.call(*args, &block)
|
|
430
|
+
end
|
|
431
|
+
completed = Queue.new
|
|
432
|
+
job = Object.new
|
|
433
|
+
job.define_singleton_method(:do_validate) { completed << true }
|
|
434
|
+
raise 'partial pool rejected work' unless Dnsruby::ValidatorThread.enqueue(job)
|
|
435
|
+
Thread.define_singleton_method(:new, original)
|
|
436
|
+
Timeout.timeout(3) { completed.pop }
|
|
437
|
+
raise 'partial workers untracked' unless Dnsruby::ValidatorThread.instance_variable_get(:@workers).count(&:alive?) == 1
|
|
438
|
+
raise 'restored admission failed' unless Dnsruby::ValidatorThread.enqueue(job)
|
|
439
|
+
Timeout.timeout(3) { completed.pop }
|
|
440
|
+
raise 'pool not replenished' unless Dnsruby::ValidatorThread.instance_variable_get(:@workers).count(&:alive?) == 4
|
|
441
|
+
RUBY
|
|
442
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', code)
|
|
443
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
444
|
+
assert_empty(stderr)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def test_nested_discovery_timeout_cancels_only_its_own_query
|
|
448
|
+
code = TestPipelineSend::FIXTURE + <<~'RUBY'
|
|
449
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
450
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', do_caching: false,
|
|
451
|
+
retry_times: 1, query_timeout: 60)
|
|
452
|
+
resolver.single_resolvers.each { |sender| sender.define_singleton_method(:send_async) { |*| } }
|
|
453
|
+
unrelated = Queue.new
|
|
454
|
+
resolver.send_async(query('unrelated.example.'), unrelated, :unrelated)
|
|
455
|
+
budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(timeout: 0.02))
|
|
456
|
+
begin
|
|
457
|
+
Dnsruby::ValidationBudget.with(budget) { resolver.query_no_validation_or_recursion('nested.example.', 'DNSKEY') }
|
|
458
|
+
raise 'discovery did not time out'
|
|
459
|
+
rescue Dnsruby::ValidationLimitError
|
|
460
|
+
end
|
|
461
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
462
|
+
raise 'nested query retained or unrelated cancelled' unless engine.instance_variable_get(:@query_list).keys == [:unrelated]
|
|
463
|
+
resolver.close
|
|
464
|
+
raise 'nested observer retained' unless K.class_variable_get(:@@observers).empty?
|
|
465
|
+
RUBY
|
|
466
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', code)
|
|
467
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
468
|
+
assert_empty(stderr)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
class TestResolverValidation
|
|
473
|
+
def test_validation_handoff_is_not_duplicated_if_delivery_crosses_deadline
|
|
474
|
+
isolated(<<~'RUBY')
|
|
475
|
+
start_query(resolver, results)
|
|
476
|
+
accept_answer(attempts.first)
|
|
477
|
+
push = SELECTOR.method(:push_validation_response_to_select)
|
|
478
|
+
SELECTOR.define_singleton_method(:push_validation_response_to_select) do |*args|
|
|
479
|
+
push.call(*args)
|
|
480
|
+
clock += 11
|
|
481
|
+
end
|
|
482
|
+
validators.shift.do_validate
|
|
483
|
+
raise 'validation delivered twice' unless K.class_variable_get(:@@queued_validation_responses).length == 1
|
|
484
|
+
SELECTOR.send_queued_validation_responses
|
|
485
|
+
expect_completion(results, Dnsruby::ResolvTimeout)
|
|
486
|
+
expect_clean(resolver)
|
|
487
|
+
RUBY
|
|
488
|
+
end
|
|
489
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require_relative 'tc_dnssec_cache_lifetime'
|
|
2
|
+
|
|
3
|
+
class KeyCacheIdentityTest
|
|
4
|
+
def test_candidate_index_keeps_collisions_and_removes_evicted_and_expired_keys
|
|
5
|
+
cache = Dnsruby::KeyCache.new(max_keys: 2)
|
|
6
|
+
signature = signed(@record, @a, 0).sigs.first
|
|
7
|
+
cache.add_key_with_expiration(@a, @now.to_i + 60)
|
|
8
|
+
cache.add_key_with_expiration(@b, @now.to_i + 120)
|
|
9
|
+
assert_equal([@a, @b], cache.matching_keys([signature]))
|
|
10
|
+
|
|
11
|
+
unrelated = @a.clone
|
|
12
|
+
unrelated.name = 'another.invalid.'
|
|
13
|
+
cache.add_key_with_expiration(unrelated, @now.to_i + 120)
|
|
14
|
+
assert_equal([@b], cache.matching_keys([signature]))
|
|
15
|
+
assert_equal([@b, unrelated], cache.keys)
|
|
16
|
+
Time.stub(:now, @now + 121) do
|
|
17
|
+
assert_empty(cache.matching_keys([signature]))
|
|
18
|
+
assert_empty(cache.keys)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_unrelated_cached_keys_do_not_consume_signature_matching_budget
|
|
23
|
+
24.times do |i|
|
|
24
|
+
key = @a.clone
|
|
25
|
+
key.name = "zone#{i}.invalid."
|
|
26
|
+
@verifier.add_trusted_key(Dnsruby::RRSet.new([key]), @now.to_i + 300)
|
|
27
|
+
end
|
|
28
|
+
@verifier.add_trust_anchor(@a)
|
|
29
|
+
budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 16))
|
|
30
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
31
|
+
assert(@verifier.verify_rrset(signed(@record, @a, 0)))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_configured_ds_index_preserves_colliding_key_identities
|
|
36
|
+
[@a, @b].each { |key| @verifier.add_trust_anchor(Dnsruby::RR::DS.from_key(key, 2)) }
|
|
37
|
+
assert(@verifier.verify(signed(@a, @a, 0)))
|
|
38
|
+
assert(@verifier.verify(signed(@b, @b, 1)))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_ds_anchor_expiration_prevents_in_flight_key_publication
|
|
42
|
+
anchor = Dnsruby::RR::DS.from_key(@a, 2)
|
|
43
|
+
@verifier.add_trust_anchor_with_expiration(anchor, @now.to_i + 1)
|
|
44
|
+
clock = @now
|
|
45
|
+
Time.stub(:now, -> { clock }) do
|
|
46
|
+
pause_after_verification(-> { clock = @now + 1 }) do
|
|
47
|
+
assert(@verifier.verify(signed(@a, @a, 0)))
|
|
48
|
+
end
|
|
49
|
+
assert_empty(@verifier.trusted_keys)
|
|
50
|
+
assert_empty(@verifier.trust_anchors)
|
|
51
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(signed(@record, @a, 0)) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_public_rrset_verification_uses_root_and_dlv_fallbacks
|
|
56
|
+
[:root_verifier, :dlv_verifier].each do |destination|
|
|
57
|
+
Dnsruby::Dnssec.reset
|
|
58
|
+
Dnsruby::Dnssec.public_send(destination).add_trust_anchor(@a)
|
|
59
|
+
assert(Dnsruby::Dnssec.verify_rrset(signed(@record, @a, 0)), destination.to_s)
|
|
60
|
+
end
|
|
61
|
+
ensure
|
|
62
|
+
Dnsruby::Dnssec.reset
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_public_rrset_fallback_keeps_one_signature_budget
|
|
66
|
+
Dnsruby::Dnssec.reset
|
|
67
|
+
Dnsruby::Dnssec.anchor_verifier.add_trust_anchor(@b)
|
|
68
|
+
Dnsruby::Dnssec.root_verifier.add_trust_anchor(@a)
|
|
69
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 1)
|
|
70
|
+
Dnsruby::Dnssec.stub(:validation_limits, limits) do
|
|
71
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
72
|
+
Dnsruby::Dnssec.verify_rrset(signed(@record, @a, 0))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
76
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
77
|
+
ensure
|
|
78
|
+
Dnsruby::Dnssec.reset
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_public_rrset_fallback_does_not_swallow_cancellation
|
|
82
|
+
visited = []
|
|
83
|
+
stopped = lambda do |*|
|
|
84
|
+
visited << :anchor
|
|
85
|
+
Dnsruby::ValidationBudget.current.cancel
|
|
86
|
+
raise Dnsruby::VerifyError, 'No usable local key'
|
|
87
|
+
end
|
|
88
|
+
root = ->(*) { visited << :root; true }
|
|
89
|
+
Dnsruby::Dnssec.anchor_verifier.stub(:verify_rrset, stopped) do
|
|
90
|
+
Dnsruby::Dnssec.root_verifier.stub(:verify_rrset, root) do
|
|
91
|
+
assert_raises(Dnsruby::ValidationCancelled) do
|
|
92
|
+
Dnsruby::Dnssec.verify_rrset(signed(@record, @a, 0))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
assert_equal([:anchor], visited)
|
|
97
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class DnssecCacheLifetimeTest
|
|
102
|
+
def test_learned_dnskey_capacity_also_bounds_provenance_and_keeps_configured_anchors
|
|
103
|
+
verifier = new_verifier
|
|
104
|
+
verifier.instance_variable_set(:@trusted_keys, Dnsruby::KeyCache.new(max_keys: 2))
|
|
105
|
+
verifier.add_trust_anchor(@parent)
|
|
106
|
+
at(0) do
|
|
107
|
+
3.times do |i|
|
|
108
|
+
key = @child.clone
|
|
109
|
+
key.name = "child#{i}.cache.invalid."
|
|
110
|
+
assert(verifier.verify(signed(key, key, 1), key))
|
|
111
|
+
end
|
|
112
|
+
assert_equal(2, verifier.trusted_keys.length)
|
|
113
|
+
assert_equal(2, verifier.instance_variable_get(:@trusted_key_provenance).length)
|
|
114
|
+
assert_equal([@parent], verifier.trust_anchors)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_learned_ds_capacity_evicts_complete_rrsets_and_keeps_configured_anchors
|
|
119
|
+
verifier = new_verifier
|
|
120
|
+
verifier.instance_variable_set(:@discovered_ds_store, Dnsruby::DSCache.new(max_records: 4))
|
|
121
|
+
verifier.add_trust_anchor(@parent)
|
|
122
|
+
at(0) do
|
|
123
|
+
3.times do |i|
|
|
124
|
+
key = @child.clone
|
|
125
|
+
key.name = "child#{i}.cache.invalid."
|
|
126
|
+
assert(verifier.verify(signed(Dnsruby::RR::DS.from_key(key, 2), @parent, 0)))
|
|
127
|
+
end
|
|
128
|
+
assert_equal(['child1.cache.invalid', 'child2.cache.invalid'], verifier.trusted_keys.map { |ds| ds.name.to_s })
|
|
129
|
+
assert_equal([@parent], verifier.trust_anchors)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_unrelated_ds_rrsets_do_not_consume_candidate_budget
|
|
134
|
+
verifier = new_verifier
|
|
135
|
+
verifier.add_trust_anchor(@parent)
|
|
136
|
+
at(0) do
|
|
137
|
+
24.times do |i|
|
|
138
|
+
key = @child.clone
|
|
139
|
+
key.name = "child#{i}.cache.invalid."
|
|
140
|
+
assert(verifier.verify(signed(Dnsruby::RR::DS.from_key(key, 2), @parent, 0)))
|
|
141
|
+
end
|
|
142
|
+
assert(verifier.verify(signed(@ds, @parent, 0)))
|
|
143
|
+
budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 16))
|
|
144
|
+
Dnsruby::ValidationBudget.with(budget) { assert(verifier.verify(signed(@child, @child, 1))) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_timed_ds_and_dlv_anchors_expire_and_invalidate_dependent_trust
|
|
149
|
+
['DS', 'DLV'].each do |type|
|
|
150
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::DLV)
|
|
151
|
+
anchor = Dnsruby::RR.create(@ds.to_s.sub(' DS ', " #{type} "))
|
|
152
|
+
verifier.add_trust_anchor_with_expiration(anchor, @now.to_i + 5)
|
|
153
|
+
verifier.add_trust_anchor(@parent)
|
|
154
|
+
identity = at(0) do
|
|
155
|
+
assert(verifier.verify(signed(@child, @child, 1)))
|
|
156
|
+
assert_cached_child(verifier)
|
|
157
|
+
verifier.validation_cache_identity
|
|
158
|
+
end
|
|
159
|
+
at(5) do
|
|
160
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify(signed_child_answer) }
|
|
161
|
+
assert_equal([@parent], verifier.trust_anchors)
|
|
162
|
+
assert_empty(verifier.trusted_keys)
|
|
163
|
+
refute_equal(identity, verifier.validation_cache_identity)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_removing_equivalent_ds_anchor_ignores_ttl_and_clears_learned_keys
|
|
169
|
+
verifier = new_verifier
|
|
170
|
+
verifier.add_trust_anchor_with_expiration(@ds, @now.to_i + 60)
|
|
171
|
+
equivalent = @ds.clone
|
|
172
|
+
equivalent.ttl += 1
|
|
173
|
+
at(0) do
|
|
174
|
+
assert(verifier.verify(signed(@child, @child, 1)))
|
|
175
|
+
assert_equal(@ds, verifier.remove_trust_anchor(equivalent))
|
|
176
|
+
assert_empty(verifier.trust_anchors)
|
|
177
|
+
assert_empty(verifier.trusted_keys)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_ds_cache_retains_complete_rrsets_for_digest_preference
|
|
182
|
+
# SHA-256 anywhere in this DS RRset suppresses SHA-1, even if only the
|
|
183
|
+
# SHA-1 key tag matches the DNSKEY being checked (RFC 4509 section 3).
|
|
184
|
+
sha1 = @ds.clone
|
|
185
|
+
sha1.digest_type = 1
|
|
186
|
+
sha1.digestbin = sha1.digest_key(@child)
|
|
187
|
+
sha1.digest = sha1.digestbin.unpack1('H*')
|
|
188
|
+
other = @ds.clone
|
|
189
|
+
other.key_tag = (@ds.key_tag + 1) % 65_536
|
|
190
|
+
verifier = new_verifier
|
|
191
|
+
verifier.add_trust_anchor(sha1)
|
|
192
|
+
verifier.add_trust_anchor(other)
|
|
193
|
+
at(0) do
|
|
194
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify(signed(@child, @child, 1)) }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|