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.
- 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 +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -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 +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- 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 +21 -2
- 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/OPT.rb +2 -3
- 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 +986 -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 +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -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 +127 -14
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'openssl'
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
class DnssecTrustRacesTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
Dnsruby::Dnssec.reset
|
|
9
|
+
@rsa = OpenSSL::PKey::RSA.generate(2048)
|
|
10
|
+
exponent = @rsa.e.to_s(2)
|
|
11
|
+
key_data = Base64.strict_encode64([exponent.bytesize].pack('C') + exponent + @rsa.n.to_s(2))
|
|
12
|
+
@key = Dnsruby::RR.create("private.invalid. 300 IN DNSKEY 257 3 8 #{key_data}")
|
|
13
|
+
@child = Dnsruby::RR.create("child.private.invalid. 300 IN DNSKEY 257 3 8 #{key_data}")
|
|
14
|
+
@keys = signed(@key, @key)
|
|
15
|
+
@child_keys = signed(@child, @child)
|
|
16
|
+
@ds = Dnsruby::RR::DS.from_key(@child, 2)
|
|
17
|
+
@ds.ttl = 300
|
|
18
|
+
@ds_set = signed(@ds, @key)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def teardown
|
|
22
|
+
Dnsruby::Dnssec.reset
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_public_verification_cannot_republish_dnskeys_after_remove_or_clear
|
|
26
|
+
[:remove, :clear].each do |operation|
|
|
27
|
+
Dnsruby::Dnssec.reset
|
|
28
|
+
Dnsruby::Dnssec.add_trust_anchor(@key)
|
|
29
|
+
assert(Dnsruby::Dnssec.verify(@keys))
|
|
30
|
+
revoke(operation)
|
|
31
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.verify(@keys) }
|
|
32
|
+
Dnsruby::Dnssec.add_trust_anchor(@key)
|
|
33
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
34
|
+
pause_after(verifier, :verify_rrset, -> { revoke(operation) }) do
|
|
35
|
+
assert(Dnsruby::Dnssec.verify(@keys))
|
|
36
|
+
end
|
|
37
|
+
assert_empty(verifier.trusted_keys)
|
|
38
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.verify(@keys) }
|
|
39
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.root_verifier.verify(@keys) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_signed_ds_cannot_republish_discovered_ds_or_matched_dnskeys
|
|
44
|
+
[:remove, :clear].product([false, true]).each do |operation, include_key|
|
|
45
|
+
Dnsruby::Dnssec.reset
|
|
46
|
+
Dnsruby::Dnssec.add_trust_anchor(@key)
|
|
47
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
48
|
+
message = if include_key
|
|
49
|
+
Dnsruby::Message.new('child.private.invalid.', 'DNSKEY').tap { |m| m.add_answer(@child) }
|
|
50
|
+
end
|
|
51
|
+
pause_after(verifier, :verify_rrset, -> { revoke(operation) }) do
|
|
52
|
+
assert(verifier.verify_ds_rrset(@ds_set, nil, message))
|
|
53
|
+
end
|
|
54
|
+
assert_empty(verifier.trusted_keys)
|
|
55
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify(@child_keys) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_configured_and_discovered_ds_cannot_publish_after_snapshot_is_revoked
|
|
60
|
+
[:remove, :clear].product([false, true]).each do |operation, discovered|
|
|
61
|
+
Dnsruby::Dnssec.reset
|
|
62
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
63
|
+
anchor = discovered ? @key : @ds
|
|
64
|
+
Dnsruby::Dnssec.add_trust_anchor(anchor)
|
|
65
|
+
assert(verifier.verify(@ds_set)) if discovered
|
|
66
|
+
# Stop after a real digest match, while check_ds_stores still owns its old
|
|
67
|
+
# snapshot; removal must not block on cryptographic or network work.
|
|
68
|
+
pause_after(discovered ? @ds_set.rrs.first : @ds, :check_key, -> { revoke(operation, anchor) }) do
|
|
69
|
+
allow_revoked_trust_rejection { verifier.check_ds_stores(@child_keys) }
|
|
70
|
+
end
|
|
71
|
+
assert_empty(verifier.trusted_keys)
|
|
72
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify(@child_keys) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_validation_carries_generation_across_anchor_lookup_and_network_discovery
|
|
77
|
+
[:remove, :clear].each do |operation|
|
|
78
|
+
Dnsruby::Dnssec.reset
|
|
79
|
+
Dnsruby::Dnssec.add_trust_anchor(@key)
|
|
80
|
+
verifier = Dnsruby::Dnssec.anchor_verifier
|
|
81
|
+
message = Dnsruby::Message.new('private.invalid.', 'DNSKEY')
|
|
82
|
+
@keys.each { |rr| message.add_answer(rr) }
|
|
83
|
+
# Stand in for the network lookup: the supplied anchor is exactly the one
|
|
84
|
+
# captured by validate before removal, and the cryptographic check is real.
|
|
85
|
+
keys = @keys
|
|
86
|
+
verifier.define_singleton_method(:follow_chain) do |anchor, _name|
|
|
87
|
+
verify(keys, anchor)
|
|
88
|
+
keys
|
|
89
|
+
end
|
|
90
|
+
pause_after(verifier, :find_closest_anchor_for, -> { revoke(operation) }) do
|
|
91
|
+
allow_revoked_trust_rejection { verifier.validate(message, message) }
|
|
92
|
+
end
|
|
93
|
+
assert_empty(verifier.trusted_keys)
|
|
94
|
+
assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.verify(@keys) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# The pause guarantees revocation occurs after the operation has captured
|
|
101
|
+
# trust. It may then finish or reject; neither outcome may publish trust.
|
|
102
|
+
def allow_revoked_trust_rejection
|
|
103
|
+
yield
|
|
104
|
+
rescue Dnsruby::VerifyError
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def revoke(operation, key = @key)
|
|
108
|
+
operation == :remove ? Dnsruby::Dnssec.remove_trust_anchor(key) : Dnsruby::Dnssec.clear_trust_anchors
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pause_after(object, method_name, mutation)
|
|
112
|
+
arrived, resume, start = Queue.new, Queue.new, Queue.new
|
|
113
|
+
original = object.method(method_name)
|
|
114
|
+
own_method = object.singleton_class.instance_methods(false).include?(method_name)
|
|
115
|
+
object.singleton_class.remove_method(method_name) if own_method
|
|
116
|
+
thread = nil
|
|
117
|
+
object.define_singleton_method(method_name) do |*args|
|
|
118
|
+
result = original.call(*args)
|
|
119
|
+
if Thread.current.equal?(thread)
|
|
120
|
+
arrived << true
|
|
121
|
+
resume.pop
|
|
122
|
+
end
|
|
123
|
+
result
|
|
124
|
+
end
|
|
125
|
+
thread = Thread.new { start.pop; yield }
|
|
126
|
+
start << true
|
|
127
|
+
Timeout.timeout(3) { arrived.pop }
|
|
128
|
+
Timeout.timeout(3) { mutation.call }
|
|
129
|
+
resume << true
|
|
130
|
+
Timeout.timeout(3) { thread.value }
|
|
131
|
+
ensure
|
|
132
|
+
object.singleton_class.remove_method(method_name) if original
|
|
133
|
+
object.define_singleton_method(method_name, original) if own_method
|
|
134
|
+
thread.kill if thread && thread.alive?
|
|
135
|
+
thread.join if thread
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def signed(record, key)
|
|
139
|
+
signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: 300,
|
|
140
|
+
type_covered: record.type, algorithm: 8, labels: record.name.labels.length,
|
|
141
|
+
original_ttl: 300, expiration: Time.now.to_i + 3600, inception: Time.now.to_i - 60,
|
|
142
|
+
key_tag: key.key_tag, signers_name: key.name)
|
|
143
|
+
data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
|
|
144
|
+
signature.signature = @rsa.sign(OpenSSL::Digest::SHA256.new, signature.sig_data + data)
|
|
145
|
+
Dnsruby::RRSet.new([record, signature])
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
require_relative 'tc_key_cache_identity'
|
|
2
|
+
|
|
3
|
+
class DnssecWildcardProofsTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
Dnsruby::Dnssec.reset
|
|
6
|
+
@private_key = OpenSSL::PKey::EC.new(KeyCacheIdentityTest::PRIVATE_KEYS.first)
|
|
7
|
+
bytes = @private_key.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
|
|
8
|
+
@zone = 'collision.invalid.'
|
|
9
|
+
@key = Dnsruby::RR.create("#{@zone} 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(bytes)}")
|
|
10
|
+
@verifier = Dnsruby::Dnssec.anchor_verifier
|
|
11
|
+
@verifier.add_trust_anchor(@key)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def teardown
|
|
15
|
+
Dnsruby::Dnssec.reset
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_full_message_requires_proof_but_standalone_rrset_verification_remains_available
|
|
19
|
+
message = wildcard_message
|
|
20
|
+
assert(@verifier.verify_rrset(message.answer.rrset(message.question.first.qname, 'A')))
|
|
21
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
22
|
+
add_nsec(message)
|
|
23
|
+
assert(@verifier.verify(message))
|
|
24
|
+
assert(Dnsruby::Dnssec.validate(message))
|
|
25
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_wildcard_and_denial_can_use_different_keys_in_the_same_zone
|
|
29
|
+
proof_key = @key.clone
|
|
30
|
+
proof_key.flags = 256
|
|
31
|
+
refute_equal(@key.key_tag, proof_key.key_tag)
|
|
32
|
+
@verifier.add_trust_anchor(proof_key)
|
|
33
|
+
message = wildcard_message
|
|
34
|
+
proof = Dnsruby::RR.create('a.collision.invalid. 300 IN NSEC z.collision.invalid. TXT NSEC RRSIG')
|
|
35
|
+
signed(proof, key: proof_key).each { |rr| message.add_authority(rr) }
|
|
36
|
+
assert(@verifier.verify(message))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_nsec_proof_does_not_need_the_answer_type_in_its_bitmap
|
|
40
|
+
message = wildcard_message
|
|
41
|
+
add_nsec(message, types: 'TXT NSEC RRSIG')
|
|
42
|
+
assert(@verifier.verify(message))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_examples_from_rfc_4035_wildcard_expansion
|
|
46
|
+
# Preserve the RFC's expanded MX owner and denial interval, with fresh
|
|
47
|
+
# signatures so the complete validation path can authenticate the proof.
|
|
48
|
+
key = Dnsruby::RR.create(@key.to_s)
|
|
49
|
+
key.name = 'example.'
|
|
50
|
+
@verifier.add_trust_anchor(key)
|
|
51
|
+
message = Dnsruby::Message.new('a.z.w.example.', 'MX')
|
|
52
|
+
message.header.qr = true
|
|
53
|
+
message.header.rcode = Dnsruby::RCode::NOERROR
|
|
54
|
+
answer = signed(Dnsruby::RR.create('*.w.example. 300 IN MX 1 ai.example.'), key: key)
|
|
55
|
+
answer.each { |rr| rr.name = 'a.z.w.example.'; message.add_answer(rr) }
|
|
56
|
+
signed(Dnsruby::RR.create('x.y.w.example. 300 IN NSEC xx.example. MX RRSIG NSEC'), key: key).
|
|
57
|
+
each { |rr| message.add_authority(rr) }
|
|
58
|
+
assert(@verifier.verify(message))
|
|
59
|
+
|
|
60
|
+
signature = answer.sigs.first
|
|
61
|
+
assert_equal(2, signature.labels)
|
|
62
|
+
signature.labels = 4
|
|
63
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
64
|
+
signature.labels = 2
|
|
65
|
+
assert(@verifier.verify(message))
|
|
66
|
+
|
|
67
|
+
# A valid signature alone cannot make an interval ending at the next closer
|
|
68
|
+
# name prove that name does not exist.
|
|
69
|
+
message.authority.clear
|
|
70
|
+
signed(Dnsruby::RR.create('x.y.w.example. 300 IN NSEC z.w.example. MX RRSIG NSEC'), key: key).
|
|
71
|
+
each { |rr| message.add_authority(rr) }
|
|
72
|
+
assert(@verifier.verify_rrset(message.authority.rrset('x.y.w.example.', 'NSEC')))
|
|
73
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_any_queries_and_cname_answers_require_their_own_wildcard_proof
|
|
77
|
+
message = wildcard_message(qtype: 'ANY')
|
|
78
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
79
|
+
add_nsec(message)
|
|
80
|
+
assert(@verifier.verify(message))
|
|
81
|
+
|
|
82
|
+
cname = wildcard_message(type: 'CNAME', data: 'target.collision.invalid.')
|
|
83
|
+
signed(Dnsruby::RR.create('target.collision.invalid. 300 IN A 192.0.2.1')).each { |rr| cname.add_answer(rr) }
|
|
84
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(cname) }
|
|
85
|
+
add_nsec(cname)
|
|
86
|
+
assert(@verifier.verify(cname))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_a_literal_wildcard_owner_has_not_been_expanded
|
|
90
|
+
message = wildcard_message(name: '*.collision.invalid.')
|
|
91
|
+
assert(@verifier.verify(message))
|
|
92
|
+
# This also exercises a genuine wildcard owner in the NSEC proof itself.
|
|
93
|
+
expanded = wildcard_message
|
|
94
|
+
add_nsec(expanded, owner: '*.collision.invalid.')
|
|
95
|
+
assert(@verifier.verify(expanded))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_signature_selection_can_use_an_exact_signature_without_a_wildcard_proof
|
|
99
|
+
message = wildcard_message
|
|
100
|
+
exact = signed(message.answer.rrset(message.question.first.qname, 'A').rrs.first)
|
|
101
|
+
message.add_answer(exact.sigs.first)
|
|
102
|
+
assert(@verifier.verify(message))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_denial_must_cover_next_closer_not_just_the_longer_question
|
|
106
|
+
message = wildcard_message(name: 'x.b.collision.invalid.')
|
|
107
|
+
# This interval covers x.b, but also proves b exists as an empty nonterminal.
|
|
108
|
+
add_nsec(message, owner: 'a.collision.invalid.', following: 'z.b.collision.invalid.')
|
|
109
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
110
|
+
|
|
111
|
+
message = wildcard_message(name: 'x.b.collision.invalid.')
|
|
112
|
+
add_nsec(message, owner: 'a.collision.invalid.', following: 'c.collision.invalid.')
|
|
113
|
+
assert(@verifier.verify(message))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_nsec_wraparound_and_strict_endpoints
|
|
117
|
+
message = wildcard_message(name: 'zz.collision.invalid.')
|
|
118
|
+
add_nsec(message, owner: 'z.collision.invalid.', following: @zone)
|
|
119
|
+
assert(@verifier.verify(message))
|
|
120
|
+
['a.collision.invalid.', 'z.collision.invalid.'].each do |endpoint|
|
|
121
|
+
message = wildcard_message(name: endpoint)
|
|
122
|
+
add_nsec(message)
|
|
123
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_unsigned_or_other_zone_denial_cannot_supply_the_proof
|
|
128
|
+
message = wildcard_message
|
|
129
|
+
message.add_authority(Dnsruby::RR.create('a.collision.invalid. 300 IN NSEC z.collision.invalid. TXT NSEC RRSIG'))
|
|
130
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
131
|
+
|
|
132
|
+
foreign = Dnsruby::RR.create(@key.to_s)
|
|
133
|
+
foreign.name = 'other.invalid.'
|
|
134
|
+
@verifier.add_trust_anchor(foreign)
|
|
135
|
+
message = wildcard_message
|
|
136
|
+
record = Dnsruby::RR.create('a.collision.invalid. 300 IN NSEC z.collision.invalid. TXT NSEC RRSIG')
|
|
137
|
+
signed(record, key: foreign).each { |rr| message.add_authority(rr) }
|
|
138
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_nsec3_proofs_with_empty_and_binary_salts
|
|
142
|
+
['-', 'a1b200ff'].each do |salt|
|
|
143
|
+
message = wildcard_message(name: 'x.b.collision.invalid.')
|
|
144
|
+
add_nsec3(message, salt: salt, next_closer: 'b.collision.invalid.')
|
|
145
|
+
assert(@verifier.verify(message))
|
|
146
|
+
assert(Dnsruby::Dnssec.validate(message))
|
|
147
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_nsec3_matching_hash_is_not_a_covering_proof
|
|
152
|
+
message = wildcard_message
|
|
153
|
+
add_nsec3(message, exact: true)
|
|
154
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_authenticated_opt_out_is_insecure_and_clears_ad
|
|
158
|
+
message = wildcard_message
|
|
159
|
+
add_nsec3(message, flags: 1)
|
|
160
|
+
assert_equal(:insecure, @verifier.verify_result(message))
|
|
161
|
+
refute(@verifier.verify(message))
|
|
162
|
+
refute(Dnsruby::Dnssec.verify(message))
|
|
163
|
+
message.security_level = Dnsruby::Message::SecurityLevel.SECURE
|
|
164
|
+
message.header.ad = true
|
|
165
|
+
# This fixture has a local trust anchor; it has no chain from the real root.
|
|
166
|
+
# An INSECURE cached result must still pass the selected validation policy.
|
|
167
|
+
Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
|
|
168
|
+
@verifier.stub(:follow_chain, Dnsruby::RRSet.new([@key])) do
|
|
169
|
+
assert(Dnsruby::Dnssec.validate(message))
|
|
170
|
+
end
|
|
171
|
+
assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
|
|
172
|
+
refute(message.header.ad)
|
|
173
|
+
|
|
174
|
+
message.header.ad = true
|
|
175
|
+
@verifier.stub(:follow_chain, Dnsruby::RRSet.new([@key])) do
|
|
176
|
+
assert(@verifier.validate(message, message))
|
|
177
|
+
end
|
|
178
|
+
assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
|
|
179
|
+
refute(message.header.ad)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_secure_proof_is_preferred_and_stale_insecure_status_does_not_survive
|
|
183
|
+
message = wildcard_message
|
|
184
|
+
add_nsec3(message, flags: 1)
|
|
185
|
+
add_nsec(message)
|
|
186
|
+
message.security_level = Dnsruby::Message::SecurityLevel.INSECURE
|
|
187
|
+
assert(Dnsruby::Dnssec.validate(message))
|
|
188
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_wildcard_proof_is_verified_once_and_keeps_its_authenticated_lifetime
|
|
192
|
+
now = Time.now
|
|
193
|
+
Time.stub(:now, now) do
|
|
194
|
+
[false, true].each do |nsec3|
|
|
195
|
+
message = wildcard_message
|
|
196
|
+
nsec3 ? add_nsec3(message) : add_nsec(message)
|
|
197
|
+
message.authority.find { |rr| rr.type == Dnsruby::Types.RRSIG }.ttl = 7
|
|
198
|
+
2.times do
|
|
199
|
+
budget = Dnsruby::ValidationBudget.new(limits:
|
|
200
|
+
Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 2))
|
|
201
|
+
Dnsruby::ValidationBudget.with(budget) { assert(@verifier.verify(message)) }
|
|
202
|
+
assert_equal([7, 7], message.authority.map(&:ttl))
|
|
203
|
+
assert(message.authority.all? do |rr|
|
|
204
|
+
rr.instance_variable_get(:@dnsruby_authenticated_expiration) == now.to_i + 7
|
|
205
|
+
end)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def signed(record, key: @key)
|
|
214
|
+
signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: 300,
|
|
215
|
+
type_covered: record.type, algorithm: 13,
|
|
216
|
+
labels: record.name.labels.length - (record.name.wild? ? 1 : 0),
|
|
217
|
+
original_ttl: 300, expiration: Time.now.to_i + 3600, inception: Time.now.to_i - 60,
|
|
218
|
+
key_tag: key.key_tag, signers_name: key.name)
|
|
219
|
+
bytes = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
|
|
220
|
+
der = @private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + bytes))
|
|
221
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
222
|
+
Dnsruby::RRSet.new([record, signature])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def wildcard_message(name: 'b.collision.invalid.', type: 'A', data: '192.0.2.1', qtype: 'A')
|
|
226
|
+
records = signed(Dnsruby::RR.create("*.collision.invalid. 300 IN #{type} #{data}"))
|
|
227
|
+
Dnsruby::Message.new(name, qtype).tap do |message|
|
|
228
|
+
message.header.qr = true
|
|
229
|
+
records.each { |rr| rr.name = name; message.add_answer(rr) }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def add_nsec(message, owner: 'a.collision.invalid.', following: 'z.collision.invalid.', types: 'TXT NSEC RRSIG')
|
|
234
|
+
signed(Dnsruby::RR.create("#{owner} 300 IN NSEC #{following} #{types}")).each { |rr| message.add_authority(rr) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_nsec3(message, salt: '-', flags: 0, next_closer: 'b.collision.invalid.', exact: false)
|
|
238
|
+
hash = Dnsruby::RR::NSEC3.calculate_hash(next_closer, 2,
|
|
239
|
+
Dnsruby::RR::NSEC3.decode_salt(salt), Dnsruby::Nsec3HashAlgorithms.SHA_1)
|
|
240
|
+
value = Dnsruby::RR::NSEC3.decode_next_hashed(hash).unpack1('H*').to_i(16)
|
|
241
|
+
hash_at = lambda do |offset|
|
|
242
|
+
bytes = [((value + offset) % (2 ** 160)).to_s(16).rjust(40, '0')].pack('H*')
|
|
243
|
+
Dnsruby::RR::NSEC3.encode_next_hashed(bytes)
|
|
244
|
+
end
|
|
245
|
+
owner, following = hash_at.call(exact ? 0 : -1), hash_at.call(1)
|
|
246
|
+
record = Dnsruby::RR.create("#{owner}.#{@zone} 300 IN NSEC3 1 #{flags} 2 #{salt} #{following} TXT")
|
|
247
|
+
signed(record).each { |rr| message.add_authority(rr) }
|
|
248
|
+
end
|
|
249
|
+
end
|
data/test/tc_ds.rb
CHANGED
|
@@ -79,8 +79,8 @@ class DsTest < Minitest::Test
|
|
|
79
79
|
|
|
80
80
|
# and check it is the same as DS
|
|
81
81
|
right_ds = Dnsruby::RR.create(DS1)
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
assert_equal(right_ds.digestbin, right_ds.digest_key(key))
|
|
83
|
+
assert(right_ds.check_key(key))
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def test_sha2
|
|
@@ -89,8 +89,8 @@ class DsTest < Minitest::Test
|
|
|
89
89
|
|
|
90
90
|
# and check it is the same as DS
|
|
91
91
|
right_ds = Dnsruby::RR.create(DS2)
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
assert_equal(right_ds.digestbin, right_ds.digest_key(key))
|
|
93
|
+
assert(right_ds.check_key(key))
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def test_dlv_from_string
|
|
@@ -104,4 +104,4 @@ class DsTest < Minitest::Test
|
|
|
104
104
|
assert(dlv2.to_s == dlv.to_s)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require_relative 'socket_helpers'
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
class EdnsRetryRegressionsTest < Minitest::Test
|
|
7
|
+
def test_formerr_then_truncated_udp_completes_over_tcp_without_mutating_caller
|
|
8
|
+
udp, tcp = TestSockets.udp_tcp_pair
|
|
9
|
+
requests = Queue.new
|
|
10
|
+
udp_worker = Thread.new do
|
|
11
|
+
2.times do |index|
|
|
12
|
+
bytes, peer = udp.recvfrom(65_535)
|
|
13
|
+
request = Dnsruby::Message.decode(bytes)
|
|
14
|
+
requests << [:udp, request]
|
|
15
|
+
reply = response(request)
|
|
16
|
+
reply.header.rcode = Dnsruby::RCode::FORMERR if index.zero?
|
|
17
|
+
reply.header.tc = true unless index.zero?
|
|
18
|
+
udp.send(reply.encode, 0, peer[3], peer[1])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
tcp_worker = Thread.new do
|
|
22
|
+
client = tcp.accept
|
|
23
|
+
length = client.read(2).unpack1('n')
|
|
24
|
+
request = Dnsruby::Message.decode(client.read(length))
|
|
25
|
+
requests << [:tcp, request]
|
|
26
|
+
reply = response(request)
|
|
27
|
+
reply.add_answer(Dnsruby::RR.create('retry.example. 0 IN A 192.0.2.44'))
|
|
28
|
+
bytes = reply.encode
|
|
29
|
+
client.write([bytes.bytesize].pack('n') + bytes)
|
|
30
|
+
ensure
|
|
31
|
+
client.close if client && !client.closed?
|
|
32
|
+
end
|
|
33
|
+
[udp_worker, tcp_worker].each { |worker| worker.report_on_exception = false }
|
|
34
|
+
|
|
35
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', port: udp.addr[1],
|
|
36
|
+
dnssec: false, do_validation: false, do_caching: false, udp_size: 4096,
|
|
37
|
+
packet_timeout: 2, query_timeout: 4, retry_times: 1)
|
|
38
|
+
query = edns_query
|
|
39
|
+
original_opt = query.get_opt
|
|
40
|
+
original_additional = query.additional.to_a.dup
|
|
41
|
+
reply = Timeout.timeout(6) { resolver.send_message(query) }
|
|
42
|
+
refute(reply.header.tc)
|
|
43
|
+
assert_equal('192.0.2.44', reply.answer.first.address.to_s)
|
|
44
|
+
refute(query.send_raw)
|
|
45
|
+
assert_same(original_opt, query.get_opt)
|
|
46
|
+
assert_equal(original_additional, query.additional)
|
|
47
|
+
Timeout.timeout(2) { udp_worker.value; tcp_worker.value }
|
|
48
|
+
observed = 3.times.map { requests.pop(true) }
|
|
49
|
+
assert_equal([:udp, :udp, :tcp], observed.map(&:first))
|
|
50
|
+
assert_equal([true, false, false], observed.map { |_, request| !!request.get_opt })
|
|
51
|
+
observed.each do |_, request|
|
|
52
|
+
assert_equal(query.header.id, request.header.id)
|
|
53
|
+
assert_equal(['keep me'], request.additional.find { |rr| rr.type == Dnsruby::Types::TXT }.strings)
|
|
54
|
+
end
|
|
55
|
+
assert(requests.empty?)
|
|
56
|
+
ensure
|
|
57
|
+
resolver.close if resolver
|
|
58
|
+
[udp_worker, tcp_worker].compact.each do |worker|
|
|
59
|
+
worker.kill if worker.alive?
|
|
60
|
+
worker.join
|
|
61
|
+
end
|
|
62
|
+
tcp.close if tcp && !tcp.closed?
|
|
63
|
+
udp.close if udp && !udp.closed?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_edns_retry_keeps_normal_response_question_validation
|
|
67
|
+
sender, retries = capturing_sender
|
|
68
|
+
query = edns_query
|
|
69
|
+
retry_query = request_edns_retry(sender, retries, query)
|
|
70
|
+
invalid = response(retry_query, 'unrelated.example.')
|
|
71
|
+
assert_equal(false, check(sender, invalid, retry_query))
|
|
72
|
+
assert_equal(1, retries.length, 'invalid response must not start another attempt')
|
|
73
|
+
assert_equal(true, check(sender, response(retry_query), retry_query))
|
|
74
|
+
refute(query.send_raw)
|
|
75
|
+
refute(retry_query.send_raw)
|
|
76
|
+
refute_nil(query.get_opt)
|
|
77
|
+
assert_nil(retry_query.get_opt)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_no_tcp_and_ignore_truncation_are_honoured_after_edns_retry
|
|
81
|
+
[{no_tcp: true}, {ignore_truncation: true}, {no_tcp: true, ignore_truncation: true}].each do |options|
|
|
82
|
+
sender, retries = capturing_sender(options)
|
|
83
|
+
retry_query = request_edns_retry(sender, retries, edns_query)
|
|
84
|
+
truncated = response(retry_query)
|
|
85
|
+
truncated.header.tc = true
|
|
86
|
+
assert_equal(true, check(sender, truncated, retry_query), options.inspect)
|
|
87
|
+
assert_equal(1, retries.length, 'transport policy must prevent a TCP retry')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_public_raw_mode_still_bypasses_protocol_retries
|
|
92
|
+
sender, retries = capturing_sender
|
|
93
|
+
query = edns_query
|
|
94
|
+
query.send_raw = true
|
|
95
|
+
[Dnsruby::RCode::FORMERR, Dnsruby::RCode::NOERROR].each do |rcode|
|
|
96
|
+
reply = response(query, 'raw-reply.example.')
|
|
97
|
+
reply.header.rcode = rcode
|
|
98
|
+
reply.header.tc = true
|
|
99
|
+
assert_equal(true, check(sender, reply, query))
|
|
100
|
+
assert_empty(retries)
|
|
101
|
+
assert(query.send_raw)
|
|
102
|
+
refute_nil(query.get_opt)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_signed_edns_retry_verifies_response_and_resigns_changed_request
|
|
107
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
108
|
+
[:sender_key, :message_key].each do |mode|
|
|
109
|
+
key = Dnsruby::RR.create(name: 'retry-key.example.', type: 'TSIG', klass: 'ANY',
|
|
110
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
111
|
+
sender, retries = capturing_sender
|
|
112
|
+
query = edns_query
|
|
113
|
+
if mode == :sender_key
|
|
114
|
+
sender.tsig = key
|
|
115
|
+
else
|
|
116
|
+
query.set_tsig(key)
|
|
117
|
+
end
|
|
118
|
+
original_bytes = sender.make_query_packet(query)
|
|
119
|
+
assert_equal(1, query.additional.count { |rr| rr.type == Dnsruby::Types::TSIG })
|
|
120
|
+
formerr = response(query)
|
|
121
|
+
formerr.header.rcode = Dnsruby::RCode::FORMERR
|
|
122
|
+
key.apply(formerr, query)
|
|
123
|
+
formerr = Dnsruby::Message.decode(formerr.encode)
|
|
124
|
+
|
|
125
|
+
assert_equal(false, check(sender, formerr, query))
|
|
126
|
+
assert(formerr.verified?)
|
|
127
|
+
assert_equal(1, retries.length)
|
|
128
|
+
retry_query, _, _, use_tcp, disable_edns = retries.first
|
|
129
|
+
assert_equal(false, use_tcp)
|
|
130
|
+
assert_equal(true, disable_edns)
|
|
131
|
+
bytes = sender.make_query_packet(retry_query, use_tcp, disable_edns)
|
|
132
|
+
decoded = Dnsruby::Message.decode(bytes)
|
|
133
|
+
assert_nil(decoded.get_opt)
|
|
134
|
+
assert_equal(1, decoded.additional.count { |rr| rr.type == Dnsruby::Types::TSIG })
|
|
135
|
+
expected = key.generate(decoded, nil, '', bytes, decoded.tsig)
|
|
136
|
+
assert_equal(expected.mac, decoded.tsig.mac, 'retry must be signed over the changed packet')
|
|
137
|
+
refute_equal(query.tsig.mac, decoded.tsig.mac)
|
|
138
|
+
assert_equal(original_bytes, query.encode, 'retry must not replace the caller\'s signed packet')
|
|
139
|
+
refute(retry_query.send_raw)
|
|
140
|
+
|
|
141
|
+
unsigned = response(retry_query)
|
|
142
|
+
assert_equal(false, check(sender, unsigned, retry_query), 'unsigned reply must leave the retry pending')
|
|
143
|
+
signed = response(retry_query)
|
|
144
|
+
key.apply(signed, decoded)
|
|
145
|
+
signed = Dnsruby::Message.decode(signed.encode)
|
|
146
|
+
assert_equal(true, check(sender, signed, retry_query))
|
|
147
|
+
assert(signed.verified?)
|
|
148
|
+
assert_equal(1, retries.length)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_failed_signing_can_be_repaired_and_retried_without_sending_unsigned
|
|
153
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
154
|
+
key = Dnsruby::RR.create(name: 'retry-key.example.', type: 'TSIG', klass: 'ANY',
|
|
155
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
156
|
+
sender, = capturing_sender
|
|
157
|
+
sender.tsig = key
|
|
158
|
+
query = edns_query
|
|
159
|
+
invalid = Dnsruby::RR.get_class(62001, 1).new('x'.b * 65_536)
|
|
160
|
+
invalid.name, invalid.ttl, invalid.type, invalid.klass = 'size.example.', 0, 62001, 1
|
|
161
|
+
query.add_additional(invalid)
|
|
162
|
+
|
|
163
|
+
assert_raises(Dnsruby::EncodeError) { sender.make_query_packet(query) }
|
|
164
|
+
assert_nil(query.tsig, 'failed signing must not leave a partial signature')
|
|
165
|
+
refute(query.signed?)
|
|
166
|
+
query.remove_additional
|
|
167
|
+
bytes = sender.make_query_packet(query)
|
|
168
|
+
decoded = Dnsruby::Message.decode(bytes)
|
|
169
|
+
assert_equal(1, decoded.additional.count { |rr| rr.type == Dnsruby::Types::TSIG })
|
|
170
|
+
assert(query.signed?)
|
|
171
|
+
expected = key.generate(decoded, nil, '', bytes, decoded.tsig)
|
|
172
|
+
assert_equal(expected.mac, decoded.tsig.mac, 'repaired request must be authenticated')
|
|
173
|
+
reply = response(query)
|
|
174
|
+
key.apply(reply, decoded)
|
|
175
|
+
reply = Dnsruby::Message.decode(reply.encode)
|
|
176
|
+
assert_equal(true, check(sender, reply, query))
|
|
177
|
+
assert(reply.verified?)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_formerr_downgrade_requires_dnssec_to_be_optional
|
|
181
|
+
[false, true].product([false, true], [false, true]).each do |dnssec, validation, signed|
|
|
182
|
+
sender, retries = capturing_sender(dnssec: dnssec)
|
|
183
|
+
query = edns_query
|
|
184
|
+
query.do_validation = validation
|
|
185
|
+
key = Dnsruby::RR.create(name: 'retry-key.example.', type: 'TSIG', klass: 'ANY',
|
|
186
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
187
|
+
query.set_tsig(key) if signed
|
|
188
|
+
original_wire = sender.make_query_packet(query)
|
|
189
|
+
formerr = response(query)
|
|
190
|
+
formerr.header.rcode = Dnsruby::RCode::FORMERR
|
|
191
|
+
key.apply(formerr, query) if signed
|
|
192
|
+
wire = formerr.encode
|
|
193
|
+
result = sender.check_response(Dnsruby::Message.decode(wire), wire, query, Queue.new, :policy, false)
|
|
194
|
+
if dnssec && validation
|
|
195
|
+
assert_equal(true, result, 'required DNSSEC must report FORMERR without removing EDNS')
|
|
196
|
+
assert_empty(retries)
|
|
197
|
+
else
|
|
198
|
+
assert_equal(false, result)
|
|
199
|
+
assert_equal(1, retries.size)
|
|
200
|
+
retry_query, _, _, tcp, disable_edns = retries.first
|
|
201
|
+
assert_nil(retry_query.get_opt)
|
|
202
|
+
retry_wire = sender.make_query_packet(retry_query, tcp, disable_edns)
|
|
203
|
+
decoded = Dnsruby::Message.decode(retry_wire)
|
|
204
|
+
assert_nil(decoded.get_opt)
|
|
205
|
+
if signed
|
|
206
|
+
assert_equal(key.generate(decoded, nil, '', retry_wire, decoded.tsig).mac, decoded.tsig.mac)
|
|
207
|
+
refute_equal(query.tsig.mac, decoded.tsig.mac)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
assert_equal(original_wire, query.encode, 'negotiation must preserve the caller request')
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def edns_query
|
|
217
|
+
Dnsruby::Message.new('retry.example.').tap do |query|
|
|
218
|
+
query.header.rd = true
|
|
219
|
+
query.do_validation = query.do_caching = false
|
|
220
|
+
query.add_additional(Dnsruby::RR::OPT.new(4096))
|
|
221
|
+
query.add_additional(Dnsruby::RR.create('extra.example. 0 IN TXT "keep me"'))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def response(query, name = query.question.first.qname)
|
|
226
|
+
Dnsruby::Message.new(name).tap do |reply|
|
|
227
|
+
reply.header.id = query.header.id
|
|
228
|
+
reply.header.qr = true
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def capturing_sender(options = {})
|
|
233
|
+
sender = Dnsruby::PacketSender.new({server: '127.0.0.1', dnssec: false,
|
|
234
|
+
udp_size: 4096}.merge(options))
|
|
235
|
+
retries = []
|
|
236
|
+
sender.define_singleton_method(:send_async) { |*arguments| retries << arguments }
|
|
237
|
+
[sender, retries]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def check(sender, reply, query)
|
|
241
|
+
sender.check_response(reply, reply.encode, query, Queue.new, :retry, false)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def request_edns_retry(sender, retries, query)
|
|
245
|
+
formerr = response(query)
|
|
246
|
+
formerr.header.rcode = Dnsruby::RCode::FORMERR
|
|
247
|
+
assert_equal(false, check(sender, formerr, query))
|
|
248
|
+
assert_equal(1, retries.length)
|
|
249
|
+
retry_query, _, id, use_tcp, disable_edns = retries.first
|
|
250
|
+
assert_equal(:retry, id)
|
|
251
|
+
assert_equal(false, use_tcp)
|
|
252
|
+
assert_equal(true, disable_edns)
|
|
253
|
+
retry_query
|
|
254
|
+
end
|
|
255
|
+
end
|