dnsruby 1.73.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -0,0 +1,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
- ds = Dnsruby::RR::DS.from_key(key, 1);
83
- assert_equal(ds.to_s, right_ds.to_s)
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
- ds = Dnsruby::RR::DS.from_key(key, 2);
93
- assert_equal(ds.to_s, right_ds.to_s)
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