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,351 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
# Ordinary records and local doubles keep these contracts independent of DNS.
|
|
7
|
+
class DnssecRecursorReviewFixesTest < Minitest::Test
|
|
8
|
+
def test_child_budgets_share_work_and_transaction_but_cancel_independently
|
|
9
|
+
root = Dnsruby::ValidationBudget.new(limits: limits(max_work: 2))
|
|
10
|
+
first, second = root.child, root.child
|
|
11
|
+
assert_same(root.transaction, first.transaction)
|
|
12
|
+
assert_same(root.transaction, second.transaction)
|
|
13
|
+
first.check!(1)
|
|
14
|
+
first.cancel
|
|
15
|
+
assert_raises(Dnsruby::ValidationCancelled) { first.check! }
|
|
16
|
+
assert(root.check!)
|
|
17
|
+
second.check!(1)
|
|
18
|
+
assert_raises(Dnsruby::ValidationLimitError) { second.check!(1) }
|
|
19
|
+
assert_raises(Dnsruby::ValidationLimitError) { root.check! }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_parallel_children_share_signature_allowance
|
|
23
|
+
root = Dnsruby::ValidationBudget.new(limits: limits(max_signature_verifications: 2))
|
|
24
|
+
threads = 2.times.map { Thread.new { root.child.signature! } }
|
|
25
|
+
assert(threads.all? { |thread| thread.join(2) })
|
|
26
|
+
threads.each(&:value)
|
|
27
|
+
assert_raises(Dnsruby::ValidationLimitError) { root.signature! }
|
|
28
|
+
ensure
|
|
29
|
+
threads&.each { |thread| thread.kill if thread.alive?; thread.join }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_parent_cancellation_reaches_children
|
|
33
|
+
root = Dnsruby::ValidationBudget.new
|
|
34
|
+
child = root.child
|
|
35
|
+
root.cancel
|
|
36
|
+
assert_raises(Dnsruby::ValidationCancelled) { child.check! }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_validator_hops_use_shared_budget_without_cancelling_the_lookup
|
|
40
|
+
root = Dnsruby::ValidationBudget.new(limits: limits(max_signature_verifications: 2))
|
|
41
|
+
transmissions = Dnsruby::SelectThread::TransmissionBudget.new
|
|
42
|
+
transmissions.context(:validation) { root }
|
|
43
|
+
controls = []
|
|
44
|
+
selector = Object.new
|
|
45
|
+
selector.define_singleton_method(:register_validation) { |_, _, control| controls << control; true }
|
|
46
|
+
selector.define_singleton_method(:unregister_validation) { |_, _, control| control.cancel }
|
|
47
|
+
selector.define_singleton_method(:push_validation_response_to_select) { |*_| }
|
|
48
|
+
query = Dnsruby::Message.new('www.example.')
|
|
49
|
+
query.do_validation = true
|
|
50
|
+
resolver = Struct.new(:dnssec).new(true)
|
|
51
|
+
validation = lambda do |_, response|
|
|
52
|
+
budget = Dnsruby::ValidationBudget.current
|
|
53
|
+
assert_same(root.transaction, budget.transaction)
|
|
54
|
+
refute_same(root.control, budget.control)
|
|
55
|
+
budget.signature!
|
|
56
|
+
response.security_level = Dnsruby::Message::SecurityLevel.SECURE
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
Dnsruby::Dnssec.stub(:validate_with_query, validation) do
|
|
60
|
+
Dnsruby::PacketSender.stub(:cache, nil) do
|
|
61
|
+
2.times do
|
|
62
|
+
response = answer('www.example.', 'www.example. 60 IN A 192.0.2.1')
|
|
63
|
+
Dnsruby::ValidatorThread.new(:client, Queue.new, response, nil, query,
|
|
64
|
+
selector, resolver, transmissions).do_validate
|
|
65
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, response.security_level)
|
|
66
|
+
assert(root.check!)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
refute_same(*controls)
|
|
71
|
+
assert_raises(Dnsruby::ValidationLimitError) { root.signature! }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_alias_query_keeps_original_question_and_one_budget_through_final_validation
|
|
75
|
+
packets = {
|
|
76
|
+
'www.example.' => answer('www.example.', 'www.example. 60 IN CNAME target.other.'),
|
|
77
|
+
'target.other.' => answer('target.other.', 'target.other. 60 IN A 192.0.2.1')
|
|
78
|
+
}
|
|
79
|
+
with_recursor(packets) do |recursor, calls, budgets|
|
|
80
|
+
validation = lambda do |response|
|
|
81
|
+
assert_same(budgets.first, Dnsruby::ValidationBudget.current)
|
|
82
|
+
assert_equal('www.example.', response.question.first.qname.to_s(true))
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
Dnsruby::Dnssec.stub(:validate, validation) do
|
|
86
|
+
response = recursor.query('www.example')
|
|
87
|
+
assert_equal(%w[www.example. target.other.], calls)
|
|
88
|
+
assert_equal([Dnsruby::Types.CNAME, Dnsruby::Types.A], response.answer.map(&:type))
|
|
89
|
+
assert_equal(1, budgets.map(&:object_id).uniq.length)
|
|
90
|
+
refute(response.header.aa)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
94
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_complete_alias_answer_uses_one_hop
|
|
98
|
+
response = answer('www.example.', 'www.example. 60 IN CNAME target.example.',
|
|
99
|
+
'target.example. 60 IN A 192.0.2.1')
|
|
100
|
+
with_recursor('www.example.' => response) do |recursor, calls|
|
|
101
|
+
assert_same(response, recursor.query('www.example.', 'A', 'IN', true))
|
|
102
|
+
assert_equal(['www.example.'], calls)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_combining_alias_answers_preserves_the_earliest_authenticated_lifetime
|
|
107
|
+
first = answer('www.example.', 'www.example. 60 IN CNAME target.other.')
|
|
108
|
+
last = answer('target.other.', 'target.other. 60 IN A 192.0.2.1')
|
|
109
|
+
first.instance_variable_set(:@dnsruby_authenticated_expiration, 100)
|
|
110
|
+
last.instance_variable_set(:@dnsruby_authenticated_expiration, 200)
|
|
111
|
+
with_recursor('www.example.' => first, 'target.other.' => last) do |recursor|
|
|
112
|
+
response = recursor.query('www.example.', 'A', 'IN', true)
|
|
113
|
+
assert_equal(100, response.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_independent_queries_get_independent_validation_budgets
|
|
118
|
+
with_recursor('www.example.' => answer('www.example.', 'www.example. 60 IN A 192.0.2.1')) do |recursor, _, budgets|
|
|
119
|
+
Dnsruby::Dnssec.stub(:validate, true) do
|
|
120
|
+
2.times { assert(recursor.query('www.example.')) }
|
|
121
|
+
end
|
|
122
|
+
refute_same(*budgets)
|
|
123
|
+
refute_same(budgets.first.transaction, budgets.last.transaction)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_dname_chase_preserves_synthesized_cname_and_original_question
|
|
128
|
+
packets = {
|
|
129
|
+
'host.branch.example.' => answer('host.branch.example.', 'branch.example. 60 IN DNAME other.'),
|
|
130
|
+
'host.other.' => answer('host.other.', 'host.other. 60 IN A 192.0.2.2')
|
|
131
|
+
}
|
|
132
|
+
with_recursor(packets) do |recursor, calls|
|
|
133
|
+
response = recursor.query('host.branch.example.', 'A', 'IN', true)
|
|
134
|
+
assert_equal(%w[host.branch.example. host.other.], calls)
|
|
135
|
+
assert_equal('host.branch.example.', response.question.first.qname.to_s(true))
|
|
136
|
+
cname = response.answer.find { |rr| rr.type == Dnsruby::Types.CNAME }
|
|
137
|
+
assert_equal('host.other.', cname.domainname.to_s(true))
|
|
138
|
+
assert_equal(60, cname.ttl)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_explicit_alias_queries_do_not_chase
|
|
143
|
+
['CNAME', 'DNAME'].each do |type|
|
|
144
|
+
packet = answer('alias.example.', "alias.example. 60 IN #{type} target.other.")
|
|
145
|
+
with_recursor('alias.example.' => packet) do |recursor, calls|
|
|
146
|
+
assert_same(packet, recursor.query('alias.example.', type, 'IN', true))
|
|
147
|
+
assert_equal(['alias.example.'], calls)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_nonvalidating_lookup_does_not_create_a_dnssec_budget
|
|
153
|
+
with_recursor('www.example.' => answer('www.example.', 'www.example. 60 IN A 192.0.2.1')) do |recursor, _, budgets|
|
|
154
|
+
Dnsruby::ValidationBudget.stub(:new, ->(*) { flunk('Nonvalidating recursion created a DNSSEC budget') }) do
|
|
155
|
+
assert(recursor.query('www.example.', 'A', 'IN', true))
|
|
156
|
+
end
|
|
157
|
+
assert_equal([nil], budgets)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_alias_steps_use_the_recursion_budget
|
|
162
|
+
packets = {
|
|
163
|
+
'www.example.' => answer('www.example.', 'www.example. 60 IN CNAME target.other.'),
|
|
164
|
+
'target.other.' => answer('target.other.', 'target.other. 60 IN CNAME final.other.')
|
|
165
|
+
}
|
|
166
|
+
with_recursor(packets) do |recursor, calls|
|
|
167
|
+
recursor.recursion_limits = { max_steps: 1 }
|
|
168
|
+
assert_raises(Dnsruby::RecursionLimitError) { recursor.query('www.example.', 'A', 'IN', true) }
|
|
169
|
+
assert_equal(%w[www.example. target.other.], calls)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_zero_ttl_addresses_are_local_and_snapshots_are_independent
|
|
174
|
+
cache = Dnsruby::Recursor::AddressCache.new
|
|
175
|
+
cache.push(['192.0.2.1', 0])
|
|
176
|
+
cache.push(['192.0.2.2', 60])
|
|
177
|
+
assert_equal(%w[192.0.2.2 192.0.2.1], cache.current_values)
|
|
178
|
+
assert_equal(['192.0.2.2'], cache.values)
|
|
179
|
+
snapshot = cache.snapshot
|
|
180
|
+
assert_equal(['192.0.2.2'], snapshot.current_values)
|
|
181
|
+
cache.push(['192.0.2.3', 60])
|
|
182
|
+
assert_equal(['192.0.2.2'], snapshot.values)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_zero_ttl_referral_glue_is_used_without_entering_shared_caches
|
|
186
|
+
with_private_caches do
|
|
187
|
+
recursor = bare_recursor
|
|
188
|
+
referral = Dnsruby::Message.new('www.child.example.')
|
|
189
|
+
referral.add_authority(Dnsruby::RR.create('child.example. 60 IN NS ns.child.example.'))
|
|
190
|
+
referral.add_additional(Dnsruby::RR.create('ns.child.example. 0 IN A 192.0.2.1'))
|
|
191
|
+
zone, authorities, ttl = recursor.referral_authorities(referral, 'example.', 'www.child.example.', 'IN')
|
|
192
|
+
Dnsruby::Recursor.send(:cache_zone, zone, authorities, ttl)
|
|
193
|
+
result = answer('www.child.example.', 'www.child.example. 60 IN A 192.0.2.2')
|
|
194
|
+
sends = []
|
|
195
|
+
factory = lambda do |settings|
|
|
196
|
+
sends << settings[:nameserver]
|
|
197
|
+
fake_resolver { |_| result }
|
|
198
|
+
end
|
|
199
|
+
Dnsruby::Resolver.stub(:new, factory) do
|
|
200
|
+
assert_same(result, recursor.send(:_dorecursion, 'www.child.example.', 'A', 'IN', zone, authorities, 0, true))
|
|
201
|
+
end
|
|
202
|
+
assert_equal([['192.0.2.1']], sends)
|
|
203
|
+
assert_empty(Dnsruby::Recursor.class_variable_get(:@@authority_cache).fetch('ns.child.example.').current_values)
|
|
204
|
+
shared = Dnsruby::Recursor.class_variable_get(:@@zones_cache).fetch(zone)
|
|
205
|
+
assert_empty(shared.fetch('ns.child.example.').current_values)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_zero_ttl_discovered_nameserver_address_is_used_once
|
|
210
|
+
with_private_caches do
|
|
211
|
+
hints = Dnsruby::Recursor::AddressCache.new
|
|
212
|
+
hints.push(['192.0.2.53', 60])
|
|
213
|
+
Dnsruby::Recursor.class_variable_set(:@@hints, { 'root.example.' => hints })
|
|
214
|
+
recursor = bare_recursor
|
|
215
|
+
sent = []
|
|
216
|
+
factory = lambda do |settings|
|
|
217
|
+
fake_resolver do |query|
|
|
218
|
+
name = query.question.first.qname.to_s(true)
|
|
219
|
+
sent << [name, settings[:nameserver]]
|
|
220
|
+
name == 'ns.other.' ? answer(name, 'ns.other. 0 IN A 192.0.2.1') :
|
|
221
|
+
answer(name, 'www.child.example. 60 IN A 192.0.2.2')
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
authorities = { 'ns.other.' => Dnsruby::Recursor::AddressCache.new }
|
|
225
|
+
Dnsruby::Resolver.stub(:new, factory) do
|
|
226
|
+
assert(recursor.send(:_dorecursion, 'www.child.example.', 'A', 'IN', 'child.example.', authorities, 0, true))
|
|
227
|
+
end
|
|
228
|
+
assert_equal([['ns.other.', ['192.0.2.53']], ['www.child.example.', ['192.0.2.1']]], sent)
|
|
229
|
+
assert_empty(Dnsruby::Recursor.class_variable_get(:@@authority_cache).fetch('ns.other.').current_values)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_signature_serial_time_across_the_epoch_boundary_and_cache_expiry
|
|
234
|
+
epoch = 1 << 32
|
|
235
|
+
now = epoch + 20
|
|
236
|
+
private_key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
237
|
+
public_key = private_key.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
|
|
238
|
+
key = Dnsruby::RR.create("example. 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(public_key)}")
|
|
239
|
+
signature = Dnsruby::RR.create(name: key.name, type: 'RRSIG', ttl: 300,
|
|
240
|
+
type_covered: 'DNSKEY', algorithm: 13, labels: 1, original_ttl: 300,
|
|
241
|
+
inception: epoch - 60, expiration: 80, key_tag: key.key_tag, signers_name: key.name)
|
|
242
|
+
wire = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(key, true) }.to_s
|
|
243
|
+
der = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + wire))
|
|
244
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
245
|
+
records = Dnsruby::RRSet.new([key, signature])
|
|
246
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
247
|
+
cache = Dnsruby::KeyCache.new
|
|
248
|
+
Time.stub(:now, Time.at(now)) do
|
|
249
|
+
assert(verifier.verify_rrset(records, key))
|
|
250
|
+
assert_equal(60, records.ttl)
|
|
251
|
+
cache.add_rrset(records)
|
|
252
|
+
assert_equal(epoch + 80, cache.keys_and_expirations.first.first)
|
|
253
|
+
end
|
|
254
|
+
Time.stub(:now, Time.at(epoch + 79)) do
|
|
255
|
+
assert_equal([Dnsruby::KeyCache.key_identity(key)], cache.keys.map { |item| Dnsruby::KeyCache.key_identity(item) })
|
|
256
|
+
end
|
|
257
|
+
Time.stub(:now, Time.at(epoch + 80)) { assert_empty(cache.keys) }
|
|
258
|
+
refute(Dnsruby::SerialTime.valid?(epoch - 60, 80, epoch + 81))
|
|
259
|
+
refute(Dnsruby::SerialTime.valid?(epoch - 60, 80, epoch - 61))
|
|
260
|
+
assert_nil(Dnsruby::SerialTime.absolute(now + (1 << 31), now))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_provisional_insecure_and_missing_cached_keys_reach_full_policy
|
|
264
|
+
[:insecure, Dnsruby::VerifyError.new('Signing key not found')].each do |cached|
|
|
265
|
+
with_full_validation(cached) do |query, response, calls|
|
|
266
|
+
assert(Dnsruby::Dnssec.validate_with_query(query, response))
|
|
267
|
+
assert_equal([:full], calls)
|
|
268
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, response.security_level)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_full_policy_bogus_is_preserved_after_provisional_insecure
|
|
274
|
+
with_full_validation(:insecure, bogus: true) do |query, response, calls|
|
|
275
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.validate_with_query(query, response) }
|
|
276
|
+
assert_equal([:full], calls)
|
|
277
|
+
assert_equal(Dnsruby::Message::SecurityLevel.BOGUS, response.security_level)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def limits(**changes)
|
|
284
|
+
Dnsruby::ValidationBudget::DEFAULT_LIMITS.merge(changes)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def answer(name, *records)
|
|
288
|
+
Dnsruby::Message.new(name).tap do |packet|
|
|
289
|
+
packet.header.qr = packet.header.aa = true
|
|
290
|
+
records.each { |record| packet.add_answer(Dnsruby::RR.create(record)) }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def bare_recursor
|
|
295
|
+
Dnsruby::Recursor.allocate.tap do |recursor|
|
|
296
|
+
recursor.instance_variable_set(:@resolver, Struct.new(:query_timeout, :recurse).new(10, false))
|
|
297
|
+
recursor.instance_variable_set(:@ipv6_ok, false)
|
|
298
|
+
recursor.instance_variable_set(:@dnssec, false)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def with_recursor(packets)
|
|
303
|
+
recursor, calls, budgets = bare_recursor, [], []
|
|
304
|
+
recursor.define_singleton_method(:recursion_cache_scope) { |_| nil }
|
|
305
|
+
recursor.define_singleton_method(:get_closest_known_zone_authorities_for) { |_| ['.', {}] }
|
|
306
|
+
recursor.define_singleton_method(:_dorecursion) do |name, *_|
|
|
307
|
+
calls << name
|
|
308
|
+
budgets << Dnsruby::ValidationBudget.current
|
|
309
|
+
packets.fetch(name)
|
|
310
|
+
end
|
|
311
|
+
Dnsruby::Recursor.stub(:ensure_hints, 0) { yield recursor, calls, budgets }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def fake_resolver(&send_message)
|
|
315
|
+
Struct.new(:dnssec, :recurse).new.tap do |resolver|
|
|
316
|
+
resolver.define_singleton_method(:send_message) { |query| send_message.call(query) }
|
|
317
|
+
resolver.define_singleton_method(:close) {}
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def with_private_caches
|
|
322
|
+
fields = [:@@authority_cache, :@@zones_cache, :@@zone_expirations, :@@hints]
|
|
323
|
+
saved = fields.to_h { |field| [field, Dnsruby::Recursor.class_variable_get(field)] }
|
|
324
|
+
fields.each { |field| Dnsruby::Recursor.class_variable_set(field, {}) }
|
|
325
|
+
yield
|
|
326
|
+
ensure
|
|
327
|
+
saved.each { |field, value| Dnsruby::Recursor.class_variable_set(field, value) }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def with_full_validation(cached, bogus: false)
|
|
331
|
+
previous = Dnsruby::Dnssec.validation_policy
|
|
332
|
+
Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
|
|
333
|
+
query = Dnsruby::Message.new('www.example.')
|
|
334
|
+
query.header.cd = true
|
|
335
|
+
response = answer('www.example.', 'www.example. 60 IN A 192.0.2.1')
|
|
336
|
+
response.add_answer(Dnsruby::RR.create(name: 'www.example.', type: 'RRSIG'))
|
|
337
|
+
calls = []
|
|
338
|
+
fast = ->(*) { cached.is_a?(Exception) ? raise(cached) : cached }
|
|
339
|
+
full = lambda do |message, _|
|
|
340
|
+
calls << :full
|
|
341
|
+
raise Dnsruby::VerifyError, 'Full validation failed' if bogus
|
|
342
|
+
message.security_level = Dnsruby::Message::SecurityLevel.SECURE
|
|
343
|
+
true
|
|
344
|
+
end
|
|
345
|
+
Dnsruby::Dnssec.stub(:verify_result, fast) do
|
|
346
|
+
Dnsruby::Dnssec.stub(:validate_with_anchors, full) { yield query, response, calls }
|
|
347
|
+
end
|
|
348
|
+
ensure
|
|
349
|
+
Dnsruby::Dnssec.validation_policy = previous
|
|
350
|
+
end
|
|
351
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
|
|
6
|
+
class DnssecTrustAnchorsTest < Minitest::Test
|
|
7
|
+
FIXTURE_DIRECTORY = File.expand_path('fixtures', __dir__)
|
|
8
|
+
FIXTURE_TIME = Time.utc(2026, 11, 5, 12)
|
|
9
|
+
ROOT_DS = [
|
|
10
|
+
'. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D',
|
|
11
|
+
'. IN DS 38696 8 2 683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16'
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def setup
|
|
15
|
+
Dnsruby::Dnssec.reset
|
|
16
|
+
@root_keys = read_fixture('root-dnskey')
|
|
17
|
+
@org_ds = read_fixture('org-ds')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def teardown
|
|
21
|
+
Dnsruby::Dnssec.reset
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_cold_start_authenticates_ksk_2024_without_reset
|
|
25
|
+
# A new process exercises the initialization path without a reset or any
|
|
26
|
+
# previously authenticated keys, regardless of the suite's random order.
|
|
27
|
+
script = <<~'RUBY'
|
|
28
|
+
require 'dnsruby'
|
|
29
|
+
require 'minitest/mock'
|
|
30
|
+
records = File.readlines(ARGV.fetch(0)).reject { |line| line.start_with?(';') || line.strip.empty? }
|
|
31
|
+
keys = Dnsruby::RRSet.new(records.map { |line| Dnsruby::RR.create(line) })
|
|
32
|
+
Time.stub(:now, Time.utc(2026, 11, 5, 12)) do
|
|
33
|
+
verifier = Dnsruby::Dnssec.root_verifier
|
|
34
|
+
abort 'unexpected startup anchors' unless verifier.trust_anchors.map(&:key_tag).sort == [20326, 38696]
|
|
35
|
+
abort 'startup cache is warm' unless verifier.get_keys_to_check.empty?
|
|
36
|
+
abort 'fixture must have only the KSK-2024 signature' unless keys.sigs.map(&:key_tag) == [38696]
|
|
37
|
+
abort 'KSK-2024 validation failed' unless verifier.verify_key_rrset(keys)
|
|
38
|
+
end
|
|
39
|
+
RUBY
|
|
40
|
+
stdout, stderr, status = Open3.capture3(
|
|
41
|
+
RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', script,
|
|
42
|
+
File.join(FIXTURE_DIRECTORY, 'root-dnskey-2026-11-05.zone')
|
|
43
|
+
)
|
|
44
|
+
assert(status.success?, "Cold-start validation failed:\n#{stdout}\n#{stderr}")
|
|
45
|
+
assert_empty(stderr)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_reset_restores_only_current_anchors_and_authenticates_ksk_2024
|
|
49
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
50
|
+
2.times do
|
|
51
|
+
Dnsruby::Dnssec.reset
|
|
52
|
+
verifier = Dnsruby::Dnssec.root_verifier
|
|
53
|
+
assert_equal(ROOT_DS.map { |text| Dnsruby::RR.create(text) }, verifier.trust_anchors)
|
|
54
|
+
assert_empty(verifier.get_keys_to_check)
|
|
55
|
+
assert_equal([38696], @root_keys.sigs.map(&:key_tag))
|
|
56
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_ksk_2017_alone_cannot_validate_the_fixture
|
|
62
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
63
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ROOT)
|
|
64
|
+
verifier.add_root_ds(Dnsruby::RR.create(ROOT_DS.first))
|
|
65
|
+
error = assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
66
|
+
assert_match(/Signing key not found/, error.message)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_verify_rrset_accepts_a_single_ds_or_dlv_without_bypassing_authentication
|
|
71
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
72
|
+
[Dnsruby::SingleVerifier::VerifierType::ROOT, Dnsruby::SingleVerifier::VerifierType::DLV].each do |mode|
|
|
73
|
+
type = mode == Dnsruby::SingleVerifier::VerifierType::DLV ? 'DLV' : 'DS'
|
|
74
|
+
ds = Dnsruby::RR.create(ROOT_DS.last.sub(' DS ', " #{type} "))
|
|
75
|
+
[ds, Dnsruby::RRSet.new([ds])].each do |keys|
|
|
76
|
+
verifier = Dnsruby::SingleVerifier.new(mode)
|
|
77
|
+
assert(verifier.verify_rrset(@root_keys, keys), "#{mode}: #{keys.class}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
wrong_digest = Dnsruby::RR.create(ROOT_DS.last.sub(' DS ', " #{type} ").sub('683D', '0000'))
|
|
81
|
+
verifier = Dnsruby::SingleVerifier.new(mode)
|
|
82
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_rrset(@root_keys, wrong_digest) }
|
|
83
|
+
assert_empty(verifier.get_keys_to_check)
|
|
84
|
+
|
|
85
|
+
corrupted = read_fixture('root-dnskey')
|
|
86
|
+
signature = corrupted.sigs.first.signature.dup
|
|
87
|
+
signature.setbyte(0, signature.getbyte(0) ^ 1)
|
|
88
|
+
corrupted.sigs.first.signature = signature
|
|
89
|
+
verifier = Dnsruby::SingleVerifier.new(mode)
|
|
90
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_rrset(corrupted, ds) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_corrupted_signature_is_rejected_with_cold_and_warm_caches
|
|
96
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
97
|
+
verifier = Dnsruby::Dnssec.root_verifier
|
|
98
|
+
corrupted = read_fixture('root-dnskey')
|
|
99
|
+
signature = corrupted.sigs.first.signature.dup
|
|
100
|
+
signature.setbyte(0, signature.getbyte(0) ^ 1)
|
|
101
|
+
corrupted.sigs.first.signature = signature
|
|
102
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(corrupted) }
|
|
103
|
+
zone_key = @root_keys.rrs.find { |key| !key.sep_key? }
|
|
104
|
+
refute_includes(verifier.get_keys_to_check.map(&:key_tag), zone_key.key_tag)
|
|
105
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
106
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(corrupted) }
|
|
107
|
+
Dnsruby::Dnssec.clear_trusted_keys
|
|
108
|
+
ROOT_DS.each { |text| verifier.add_root_ds(Dnsruby::RR.create(text)) }
|
|
109
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(corrupted) }
|
|
110
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_cache_clear_removes_root_anchors_and_learned_dnskeys_and_ds
|
|
115
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
116
|
+
verifier = Dnsruby::Dnssec.root_verifier
|
|
117
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
118
|
+
assert(verifier.verify_ds_rrset(@org_ds))
|
|
119
|
+
assert_includes(verifier.trusted_keys, @org_ds.rrs.first)
|
|
120
|
+
refute_empty(verifier.get_keys_to_check)
|
|
121
|
+
|
|
122
|
+
Dnsruby::Dnssec.clear_trusted_keys
|
|
123
|
+
|
|
124
|
+
assert_empty(verifier.trust_anchors)
|
|
125
|
+
assert_empty(verifier.trusted_keys)
|
|
126
|
+
assert_empty(verifier.get_keys_to_check)
|
|
127
|
+
assert(Dnsruby::Dnssec.no_keys?)
|
|
128
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_ds_rrset(@org_ds) }
|
|
129
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_cache_clear_retains_custom_dnskey_anchors_but_removes_ds_and_dlv_anchors
|
|
134
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
135
|
+
key = signing_key
|
|
136
|
+
ds = Dnsruby::RR.create(ROOT_DS.last)
|
|
137
|
+
dlv = Dnsruby::RR.create(ROOT_DS.last.sub(' DS ', ' DLV '))
|
|
138
|
+
Dnsruby::Dnssec.add_trust_anchor(key)
|
|
139
|
+
Dnsruby::Dnssec.add_trust_anchor(ds)
|
|
140
|
+
Dnsruby::Dnssec.dlv_verifier.add_trust_anchor(dlv)
|
|
141
|
+
assert(Dnsruby::Dnssec.anchor_verifier.verify_key_rrset(@root_keys))
|
|
142
|
+
assert(Dnsruby::Dnssec.dlv_verifier.verify_key_rrset(@root_keys))
|
|
143
|
+
|
|
144
|
+
Dnsruby::Dnssec.clear_trusted_keys
|
|
145
|
+
|
|
146
|
+
assert_equal([key], Dnsruby::Dnssec.trust_anchors)
|
|
147
|
+
assert_equal([key], Dnsruby::Dnssec.anchor_verifier.get_keys_to_check)
|
|
148
|
+
assert_empty(Dnsruby::Dnssec.dlv_verifier.trust_anchors)
|
|
149
|
+
assert_empty(Dnsruby::Dnssec.dlv_verifier.get_keys_to_check)
|
|
150
|
+
assert(Dnsruby::Dnssec.anchor_verifier.verify_key_rrset(@root_keys))
|
|
151
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.dlv_verifier.verify_key_rrset(@root_keys) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_public_clear_sequence_removes_local_and_root_trust
|
|
156
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
157
|
+
# Existing clients use this sequence before supplying their own isolated
|
|
158
|
+
# trust anchors. Retaining root trust here would change their validation.
|
|
159
|
+
Dnsruby::Dnssec.add_trust_anchor(signing_key)
|
|
160
|
+
Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ROOT_DS.last))
|
|
161
|
+
assert(Dnsruby::Dnssec.root_verifier.verify_key_rrset(@root_keys))
|
|
162
|
+
assert(Dnsruby::Dnssec.anchor_verifier.verify_key_rrset(@root_keys))
|
|
163
|
+
|
|
164
|
+
Dnsruby::Dnssec.clear_trust_anchors
|
|
165
|
+
Dnsruby::Dnssec.clear_trusted_keys
|
|
166
|
+
|
|
167
|
+
assert_empty(Dnsruby::Dnssec.trust_anchors)
|
|
168
|
+
assert_empty(Dnsruby::Dnssec.root_verifier.trust_anchors)
|
|
169
|
+
assert(Dnsruby::Dnssec.no_keys?)
|
|
170
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.root_verifier.verify_key_rrset(@root_keys) }
|
|
171
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.anchor_verifier.verify_key_rrset(@root_keys) }
|
|
172
|
+
|
|
173
|
+
Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
|
|
174
|
+
query = Dnsruby::Message.new('.', 'DNSKEY')
|
|
175
|
+
message = Dnsruby::Message.new('.', 'DNSKEY')
|
|
176
|
+
@root_keys.rrs.each { |rr| message.add_answer(rr) }
|
|
177
|
+
@root_keys.sigs.each { |rr| message.add_answer(rr) }
|
|
178
|
+
Dnsruby::Dnssec.validate_with_query(query, message)
|
|
179
|
+
assert_equal('UNCHECKED', message.security_level.to_s)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_explicit_clear_removes_custom_anchors_and_derived_trust
|
|
184
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
185
|
+
Dnsruby::Dnssec.add_trust_anchor(signing_key)
|
|
186
|
+
Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ROOT_DS.last))
|
|
187
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
188
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
189
|
+
assert(verifier.verify_ds_rrset(@org_ds))
|
|
190
|
+
|
|
191
|
+
Dnsruby::Dnssec.clear_trust_anchors
|
|
192
|
+
|
|
193
|
+
assert_empty(Dnsruby::Dnssec.trust_anchors)
|
|
194
|
+
assert_empty(verifier.trusted_keys)
|
|
195
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
196
|
+
assert_equal([20326, 38696], Dnsruby::Dnssec.root_verifier.trust_anchors.map(&:key_tag))
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_remove_ds_anchor_invalidates_derived_trust_and_retains_other_anchors
|
|
201
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
202
|
+
old_ds, new_ds = ROOT_DS.map { |text| Dnsruby::RR.create(text) }
|
|
203
|
+
Dnsruby::Dnssec.add_trust_anchor(old_ds)
|
|
204
|
+
Dnsruby::Dnssec.add_trust_anchor(new_ds)
|
|
205
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
206
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
207
|
+
assert(verifier.verify_ds_rrset(@org_ds))
|
|
208
|
+
|
|
209
|
+
Dnsruby::Dnssec.remove_trust_anchor(new_ds)
|
|
210
|
+
|
|
211
|
+
assert_equal([old_ds], Dnsruby::Dnssec.trust_anchors)
|
|
212
|
+
assert_equal([old_ds], verifier.trusted_keys)
|
|
213
|
+
assert_empty(verifier.get_keys_to_check)
|
|
214
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_remove_equivalent_dnskey_anchor_invalidates_derived_trust
|
|
219
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
220
|
+
key = signing_key
|
|
221
|
+
Dnsruby::Dnssec.add_trust_anchor(key)
|
|
222
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
223
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
224
|
+
|
|
225
|
+
equivalent_key = Dnsruby::RR.create(key.to_s)
|
|
226
|
+
equivalent_key.ttl += 1 # TTL is not part of a DNSKEY's identity.
|
|
227
|
+
refute_same(key, equivalent_key)
|
|
228
|
+
Dnsruby::Dnssec.remove_trust_anchor(equivalent_key)
|
|
229
|
+
|
|
230
|
+
assert_empty(Dnsruby::Dnssec.trust_anchors)
|
|
231
|
+
assert_empty(verifier.trusted_keys)
|
|
232
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_removing_unconfigured_key_with_same_tag_preserves_trust
|
|
237
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
238
|
+
key = signing_key
|
|
239
|
+
Dnsruby::Dnssec.add_trust_anchor(key)
|
|
240
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
241
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
242
|
+
cached_keys = verifier.trusted_keys
|
|
243
|
+
foreign_key = key.clone
|
|
244
|
+
foreign_key.name = 'example.' # Owner name is not included in the key tag.
|
|
245
|
+
assert_equal(key.key_tag, foreign_key.key_tag)
|
|
246
|
+
|
|
247
|
+
Dnsruby::Dnssec.remove_trust_anchor(foreign_key)
|
|
248
|
+
|
|
249
|
+
assert_equal([key], Dnsruby::Dnssec.trust_anchors)
|
|
250
|
+
assert_equal(cached_keys, verifier.trusted_keys)
|
|
251
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def test_explicit_remove_and_clear_work_for_dlv_anchors
|
|
256
|
+
Time.stub(:now, FIXTURE_TIME) do
|
|
257
|
+
verifier = Dnsruby::Dnssec.dlv_verifier
|
|
258
|
+
dlv = Dnsruby::RR.create(ROOT_DS.last.sub(' DS ', ' DLV '))
|
|
259
|
+
verifier.add_trust_anchor(dlv)
|
|
260
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
261
|
+
verifier.remove_trust_anchor(dlv)
|
|
262
|
+
assert_empty(verifier.trust_anchors)
|
|
263
|
+
assert_empty(verifier.trusted_keys)
|
|
264
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
265
|
+
|
|
266
|
+
verifier.add_trust_anchor(dlv)
|
|
267
|
+
assert(verifier.verify_key_rrset(@root_keys))
|
|
268
|
+
verifier.clear_trust_anchors
|
|
269
|
+
assert_empty(verifier.trust_anchors)
|
|
270
|
+
assert_empty(verifier.trusted_keys)
|
|
271
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify_key_rrset(@root_keys) }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def read_fixture(name)
|
|
278
|
+
lines = File.readlines(File.join(FIXTURE_DIRECTORY, "#{name}-2026-11-05.zone"))
|
|
279
|
+
records = lines.reject { |line| line.start_with?(';') || line.strip.empty? }
|
|
280
|
+
Dnsruby::RRSet.new(records.map { |line| Dnsruby::RR.create(line) })
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def signing_key
|
|
284
|
+
@root_keys.rrs.find { |key| key.key_tag == 38696 }
|
|
285
|
+
end
|
|
286
|
+
end
|