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,442 @@
1
+ require_relative 'tc_dnssec_alias_validation'
2
+
3
+ class DnssecAliasValidationTest
4
+ def test_signed_alias_to_unsigned_target_authenticates_target_delegation
5
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.'),
6
+ Dnsruby::RRSet.new([Dnsruby::RR.create('target.right. 300 IN A 192.0.2.1')])])
7
+ message.header.ad = true
8
+ with_chain_resolver(responses: unsigned_delegation) do |queries, budgets|
9
+ assert(@verifier.validate(message, nil))
10
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
11
+ refute(message.header.ad)
12
+ assert_includes(queries, ['right.', 'DS'])
13
+ refute_includes(queries, ['right.', 'DNSKEY'])
14
+ assert_equal(1, budgets.uniq.length)
15
+ end
16
+ end
17
+
18
+ def test_signed_alias_to_unsigned_negative_target_accepts_unsigned_soa
19
+ [Dnsruby::RCode.NOERROR, Dnsruby::RCode.NXDOMAIN].each do |rcode|
20
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME absent.right.', 'left.')])
21
+ message.header.rcode = rcode
22
+ message.add_authority(Dnsruby::RR.create('right. 300 IN SOA ns.right. hostmaster.right. 1 3600 600 86400 300'))
23
+ with_chain_resolver(responses: unsigned_delegation) do |_queries, _budgets|
24
+ assert(@verifier.validate(message, nil))
25
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
26
+ end
27
+ end
28
+ end
29
+
30
+ def test_unsigned_alias_target_still_needs_authenticated_delegation
31
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.'),
32
+ Dnsruby::RRSet.new([Dnsruby::RR.create('target.right. 300 IN A 192.0.2.1')])])
33
+ denial = Dnsruby::Message.new('right.', 'DS')
34
+ with_chain_resolver(responses: {['right.', 'DS'] => denial}) do |_queries, _budgets|
35
+ assert_raises(Dnsruby::VerifyError) { @verifier.validate(message, nil) }
36
+ end
37
+ end
38
+
39
+ def test_unsigned_alias_positive_cache_lifetime_is_bounded_by_no_ds_proof
40
+ clock = @now
41
+ Time.stub(:now, -> { Time.at(clock) }) do
42
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.'),
43
+ Dnsruby::RRSet.new([Dnsruby::RR.create('target.right. 300 IN A 192.0.2.1')])])
44
+ with_chain_resolver(responses: unsigned_delegation(lifetime: 7)) do |_queries, _budgets|
45
+ assert(@verifier.validate(message, nil))
46
+ end
47
+ assert_equal(@now + 7, message.instance_variable_get(:@dnsruby_authenticated_expiration))
48
+ cache = Dnsruby::Cache.new(preserve_security: true)
49
+ cache.add(message)
50
+ clock = @now + 6
51
+ refute_nil(cache.find('alias.left.', 'A'))
52
+ clock = @now + 7
53
+ assert_nil(cache.find('alias.left.', 'A'))
54
+ end
55
+ end
56
+
57
+ def test_unsigned_alias_negative_cache_lifetime_includes_proof_trust_chain
58
+ clock = @now
59
+ Time.stub(:now, -> { Time.at(clock) }) do
60
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME absent.right.', 'left.')])
61
+ message.header.rcode = Dnsruby::RCode.NXDOMAIN
62
+ message.add_authority(Dnsruby::RR.create('right. 300 IN SOA ns.right. hostmaster.right. 1 3600 600 86400 300'))
63
+ responses = unsigned_delegation(lifetime: 7).merge(
64
+ ['.', 'DNSKEY'] => message_with('.', 'DNSKEY', [signed(@keys.fetch('.').first, '.', lifetime: 5)]))
65
+ with_chain_resolver(responses: responses) { |_queries, _budgets| assert(@verifier.validate(message, nil)) }
66
+ assert_equal(@now + 5, message.instance_variable_get(:@dnsruby_authenticated_expiration))
67
+ cache = Dnsruby::Cache.new(preserve_security: true)
68
+ cache.add(message)
69
+ clock = @now + 4
70
+ refute_nil(cache.find('alias.left.', 'A'))
71
+ clock = @now + 5
72
+ assert_nil(cache.find('alias.left.', 'A'))
73
+ end
74
+ end
75
+
76
+ def test_unsigned_alias_insecurity_expiration_tracks_authenticated_unsupported_ds
77
+ Time.stub(:now, Time.at(@now)) do
78
+ message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.'),
79
+ Dnsruby::RRSet.new([Dnsruby::RR.create('target.right. 300 IN A 192.0.2.1')])])
80
+ ds = Dnsruby::RR.create('right. 300 IN DS 12345 5 2 ' + '01' * 32)
81
+ responses = {['right.', 'DS'] => message_with('right.', 'DS', [signed(ds, '.', lifetime: 7)])}
82
+ with_chain_resolver(responses: responses) { |_queries, _budgets| assert(@verifier.validate(message, nil)) }
83
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
84
+ assert_equal(@now + 7, message.instance_variable_get(:@dnsruby_authenticated_expiration))
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def unsigned_delegation(lifetime: 3600)
91
+ denial = Dnsruby::Message.new('right.', 'DS')
92
+ signed('right. 300 IN NSEC z. NS NSEC RRSIG', '.', lifetime: lifetime).each { |rr| denial.add_authority(rr) }
93
+ {['right.', 'DS'] => denial}
94
+ end
95
+ end
96
+
97
+ class V2ReleaseDnssecTest < Minitest::Test
98
+ def setup
99
+ @verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
100
+ end
101
+
102
+ def test_rsa_key_and_algorithm_mutation_refresh_public_key_and_length
103
+ first = rsa_dnskey(OpenSSL::PKey::RSA.generate(1024), 8)
104
+ second = rsa_dnskey(OpenSSL::PKey::RSA.generate(2048), 8)
105
+ old = first.public_key
106
+ first.key = Base64.strict_encode64(second.key).freeze
107
+ refute_equal(old.n, first.public_key.n)
108
+ assert_equal(second.public_key.n, first.public_key.n)
109
+ assert_equal(2048, first.key_length)
110
+ current = first.public_key
111
+ first.algorithm = 10
112
+ assert_nil(first.key_length)
113
+ refute_same(current, first.public_key)
114
+ assert_equal(second.public_key.n, first.public_key.n)
115
+ assert_equal(2048, first.key_length)
116
+ end
117
+
118
+ def test_sha1_crypto_remains_available_but_configured_legacy_anchors_are_insecure
119
+ private_key = OpenSSL::PKey::RSA.generate(1024)
120
+ [5, 7].each do |algorithm|
121
+ key = rsa_dnskey(private_key, algorithm)
122
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
123
+ verifier.add_trust_anchor(key)
124
+ record = Dnsruby::RR.create('www.example. 60 IN A 192.0.2.1')
125
+ signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: 60,
126
+ type_covered: 'A', algorithm: algorithm, labels: 2, original_ttl: 60,
127
+ expiration: Time.now.to_i + 600, inception: Time.now.to_i - 60,
128
+ key_tag: key.key_tag, signers_name: key.name)
129
+ data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
130
+ signature.signature = private_key.sign(OpenSSL::Digest::SHA1.new, signature.sig_data + data)
131
+ rrset = Dnsruby::RRSet.new([record, signature])
132
+ assert(verifier.verify(rrset, key))
133
+ message = Dnsruby::Message.new('www.example.', 'A')
134
+ rrset.each { |rr| message.add_answer(rr) }
135
+ message.header.ad = true
136
+ assert(verifier.validate(message, nil))
137
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
138
+ refute(message.header.ad)
139
+ # Explicit verification remains available after the operational call.
140
+ assert(verifier.verify(rrset, key))
141
+ ds = Dnsruby::RR.create(name: key.name, type: 'DS', klass: key.klass, ttl: 60,
142
+ key_tag: key.key_tag, algorithm: algorithm, digest_type: 2)
143
+ ds.digestbin = ds.digest_key(key)
144
+ ds.digest = ds.digestbin.unpack1('H*')
145
+ key_signature = signature.clone
146
+ key_signature.name, key_signature.type_covered, key_signature.labels = key.name, Dnsruby::Types.DNSKEY, 1
147
+ key_data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(key, true) }.to_s
148
+ key_signature.signature = private_key.sign(OpenSSL::Digest::SHA1.new, key_signature.sig_data + key_data)
149
+ assert(verifier.verify(Dnsruby::RRSet.new([key, key_signature]), ds))
150
+ end
151
+ end
152
+
153
+ def test_rsa_integer_encoding_and_protocol_bounds_are_checked_before_openssl
154
+ exponent = "\x01\x00\x01".b
155
+ modulus = "\x80".b + "\x01".b * 63
156
+ cases = [
157
+ [8, "\0".b + [513].pack('n') + "\x01".b * 513 + modulus],
158
+ [8, [3].pack('C') + exponent + "\x80".b + "\x01".b * 512],
159
+ [8, [4].pack('C') + "\0".b + exponent + modulus],
160
+ [8, [3].pack('C') + exponent + "\0".b + modulus],
161
+ [8, [3].pack('C') + exponent + modulus.byteslice(0, 63)],
162
+ [10, [3].pack('C') + exponent + modulus]
163
+ ]
164
+ OpenSSL::PKey::RSA.stub(:new, ->(*) { flunk('invalid DNSKEY reached OpenSSL') }) do
165
+ cases.each do |algorithm, bytes|
166
+ record = Dnsruby::RR::IN::DNSKEY.new([257, 3, algorithm, bytes])
167
+ assert_raises(OpenSSL::PKey::PKeyError) { record.public_key }
168
+ end
169
+ end
170
+ end
171
+
172
+ def test_legacy_only_authenticated_ds_records_are_operationally_unsupported
173
+ [5, 7].each do |algorithm|
174
+ record = Dnsruby::RR.create("example. 60 IN DS 12345 #{algorithm} 2 #{'01' * 32}")
175
+ assert_equal([false, nil], @verifier.get_anchor_for(record.name, record.name, record))
176
+ end
177
+ end
178
+
179
+ def test_new_ds_delegations_default_to_sha256_and_reject_deprecated_algorithms
180
+ private_key = OpenSSL::PKey::RSA.generate(1024)
181
+ modern = rsa_dnskey(private_key, 8)
182
+ default = Dnsruby::RR::DS.from_key(modern)
183
+ assert_equal(2, default.digest_type.code)
184
+ assert(default.check_key(modern))
185
+ assert(Dnsruby::RR::DS.from_key(modern, 4).check_key(modern))
186
+ assert_raises(ArgumentError) { Dnsruby::RR::DS.from_key(modern, 1) }
187
+ [5, 7].each do |algorithm|
188
+ legacy = rsa_dnskey(private_key, algorithm)
189
+ assert_raises(ArgumentError) { Dnsruby::RR::DS.from_key(legacy) }
190
+ assert_raises(ArgumentError) { Dnsruby::RR::DS.from_key(legacy, 2) }
191
+ end
192
+ end
193
+
194
+ def test_ds_digest_preference_applies_to_explicit_configured_and_rrset_checks
195
+ keys = [1024, 2048].map { |bits| rsa_dnskey(OpenSSL::PKey::RSA.generate(bits), 8) }
196
+ # Legacy DS digest verification remains supported, even though publishing
197
+ # new SHA-1 delegations through from_key is prohibited.
198
+ sha1 = Dnsruby::RR::DS.from_key(keys.first)
199
+ sha1.digest_type = 1
200
+ sha1.digestbin = sha1.digest_key(keys.first)
201
+ sha1.digest = sha1.digestbin.unpack1('H*')
202
+ sha256 = Dnsruby::RR::DS.from_key(keys.last, 2)
203
+ rrset = Dnsruby::RRSet.new(keys)
204
+ dsset = Dnsruby::RRSet.new([sha1, sha256])
205
+ candidates = @verifier.send(:ds_candidate_keys, rrset, dsset)
206
+ assert_equal([keys.last.key], candidates.values.map { |key, _| key.key })
207
+ [sha1, sha256].each { |ds| @verifier.add_trust_anchor(ds) }
208
+ assert_equal([keys.last.key], @verifier.send(:ds_candidate_keys, rrset).values.map { |key, _| key.key })
209
+ anchor = @verifier.find_closest_anchor_for(keys.first.name)
210
+ assert_instance_of(Dnsruby::RRSet, anchor)
211
+ assert_equal(2, anchor.rrs.length)
212
+ assert_equal([keys.last.key], @verifier.send(:ds_candidate_keys, rrset, anchor).values.map { |key, _| key.key })
213
+ assert(@verifier.check_ds(keys.last, dsset, Time.now.to_i + 60))
214
+ refute(@verifier.check_ds(keys.first, dsset, Time.now.to_i + 60))
215
+ assert(@verifier.check_ds(keys.first, Dnsruby::RRSet.new([sha1]), Time.now.to_i + 60))
216
+ end
217
+
218
+ def test_nsec3_and_nsec3param_round_trip_full_salt_length_range
219
+ [0, 127, 128, 255].each do |length|
220
+ salt = length.zero? ? '-' : 'ab' * length
221
+ ['NSEC3PARAM 1 0 0 ' + salt,
222
+ 'NSEC3 1 0 0 ' + salt + ' 00000000000000000000000000000000 A RRSIG'].each do |rdata|
223
+ record = Dnsruby::RR.create('example. 60 IN ' + rdata)
224
+ message = Dnsruby::Message.new
225
+ message.add_answer(record)
226
+ decoded = Dnsruby::Message.decode(message.encode).answer.first
227
+ assert_equal(length, decoded.salt_length)
228
+ assert_equal(salt, decoded.salt)
229
+ assert_equal(record.to_s, decoded.to_s)
230
+ end
231
+ end
232
+ end
233
+
234
+ def test_cache_identity_tracks_trust_addition_removal_and_expiry
235
+ before = @verifier.validation_cache_identity
236
+ key = rsa_dnskey(OpenSSL::PKey::RSA.generate(1024), 8)
237
+ @verifier.add_trust_anchor(key)
238
+ added = @verifier.validation_cache_identity
239
+ refute_equal(before, added)
240
+ assert_equal(added, @verifier.validation_cache_identity)
241
+ @verifier.remove_trust_anchor(key)
242
+ refute_equal(added, @verifier.validation_cache_identity)
243
+ now = Time.now.to_i
244
+ @verifier.add_trust_anchor_with_expiration(key, now + 1)
245
+ current = @verifier.validation_cache_identity
246
+ Time.stub(:now, Time.at(now + 2)) { refute_equal(current, @verifier.validation_cache_identity) }
247
+ end
248
+
249
+ def test_unknown_nsec3_hash_with_unsigned_length_round_trips
250
+ record = Dnsruby::RR::IN::NSEC3.new([254, 0, 0, 0, ''.b, 128, "\x01".b * 128, []])
251
+ record.name = 'example.'
252
+ record.type, record.klass, record.ttl = Dnsruby::Types.NSEC3, Dnsruby::Classes.IN, 60
253
+ message = Dnsruby::Message.new
254
+ message.add_answer(record)
255
+ decoded = Dnsruby::Message.decode(message.encode).answer.first
256
+ assert_equal(254, decoded.hash_alg.code)
257
+ assert_equal(128, decoded.hash_length)
258
+ assert_equal(record.next_hashed, decoded.next_hashed)
259
+ end
260
+
261
+ private
262
+
263
+ def rsa_dnskey(private_key, algorithm)
264
+ exponent = private_key.e.to_s(2)
265
+ bytes = [exponent.bytesize].pack('C') + exponent + private_key.n.to_s(2)
266
+ Dnsruby::RR.create("example. 60 IN DNSKEY 257 3 #{algorithm} #{Base64.strict_encode64(bytes)}")
267
+ end
268
+ end
269
+
270
+ module V2PublicDnssecHelper
271
+ def with_public_verifier(verifier)
272
+ names = %i[@@anchor_verifier @@root_verifier @@dlv_verifier]
273
+ previous = names.to_h { |name| [name, Dnsruby::Dnssec.class_variable_get(name)] }
274
+ policy = Dnsruby::Dnssec.validation_policy
275
+ Dnsruby::Dnssec.class_variable_set(:@@anchor_verifier, verifier)
276
+ Dnsruby::Dnssec.class_variable_set(:@@root_verifier,
277
+ Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ROOT))
278
+ Dnsruby::Dnssec.class_variable_set(:@@dlv_verifier,
279
+ Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::DLV))
280
+ Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
281
+ yield
282
+ ensure
283
+ previous.each { |name, saved| Dnsruby::Dnssec.class_variable_set(name, saved) }
284
+ Dnsruby::Dnssec.validation_policy = policy
285
+ end
286
+ end
287
+
288
+ class V2ReleaseDnssecTest
289
+ include V2PublicDnssecHelper
290
+
291
+ def test_public_validation_fast_path_applies_legacy_algorithm_policy
292
+ private_key = OpenSSL::PKey::RSA.generate(1024)
293
+ [5, 7].each do |algorithm|
294
+ key = rsa_dnskey(private_key, algorithm)
295
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
296
+ verifier.add_trust_anchor(key)
297
+ records = signed_rsa('www.example. 60 IN A 192.0.2.1', key, private_key)
298
+ message = Dnsruby::Message.new('www.example.', 'A')
299
+ records.each { |rr| message.add_answer(rr) }
300
+ message.header.ad = true
301
+ query = Dnsruby::Message.new('www.example.', 'A')
302
+ query.header.cd = true
303
+ with_public_verifier(verifier) do
304
+ assert(Dnsruby::Dnssec.verify(records, key))
305
+ assert(Dnsruby::Dnssec.validate_with_query(query, message))
306
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
307
+ refute(message.header.ad)
308
+ assert(Dnsruby::Dnssec.verify(records, key))
309
+ end
310
+ end
311
+ end
312
+
313
+ def test_historical_verification_cannot_seed_operational_trust_for_modern_descendants
314
+ legacy_private = OpenSSL::PKey::RSA.generate(1024)
315
+ modern_private = OpenSSL::PKey::RSA.generate(1024)
316
+ legacy = rsa_dnskey(legacy_private, 5)
317
+ modern = rsa_dnskey(modern_private, 8)
318
+ modern.name = 'child.example.'
319
+ @verifier.add_trust_anchor(legacy)
320
+ ds_records = signed_rsa(Dnsruby::RR::DS.from_key(modern), legacy, legacy_private)
321
+ keys = signed_rsa(modern, modern, modern_private)
322
+ answer = signed_rsa('www.child.example. 60 IN A 192.0.2.1', modern, modern_private)
323
+ message = Dnsruby::Message.new('www.child.example.', 'A')
324
+ answer.each { |rr| message.add_answer(rr) }
325
+ query = Dnsruby::Message.new('www.child.example.', 'A')
326
+ query.header.cd = true
327
+ with_public_verifier(@verifier) do
328
+ assert(Dnsruby::Dnssec.verify(ds_records))
329
+ assert(Dnsruby::Dnssec.verify(keys))
330
+ assert(Dnsruby::Dnssec.verify(answer))
331
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_for_validation(message) }
332
+ assert(Dnsruby::Dnssec.validate_with_query(query, message))
333
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
334
+ end
335
+ end
336
+
337
+ def test_public_validation_fast_path_obeys_exclusive_anchor_policy
338
+ private_key = OpenSSL::PKey::RSA.generate(1024)
339
+ key = rsa_dnskey(private_key, 8)
340
+ @verifier.add_trust_anchor(key)
341
+ records = signed_rsa('www.example. 60 IN A 192.0.2.1', key, private_key)
342
+ policies = [Dnsruby::Dnssec::ValidationPolicy::ALWAYS_ROOT_ONLY,
343
+ Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY]
344
+ policies.each do |policy|
345
+ message = Dnsruby::Message.new('www.example.', 'A')
346
+ records.each { |rr| message.add_answer(rr) }
347
+ with_public_verifier(@verifier) do
348
+ if policy == Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
349
+ Dnsruby::Dnssec.class_variable_set(:@@root_verifier, @verifier)
350
+ Dnsruby::Dnssec.class_variable_set(:@@anchor_verifier,
351
+ Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR))
352
+ end
353
+ # A configured DLV key is also outside either exclusive policy.
354
+ Dnsruby::Dnssec.class_variable_set(:@@dlv_verifier, @verifier)
355
+ Dnsruby::Dnssec.validation_policy = policy
356
+ assert(Dnsruby::Dnssec.verify(records))
357
+ assert(Dnsruby::Dnssec.validate(message))
358
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
359
+ end
360
+ end
361
+ end
362
+
363
+ def test_nsec3_binary_hyphen_salt_round_trips
364
+ %w[NSEC3 NSEC3PARAM].each do |type|
365
+ text = "example. 60 IN #{type} 1 0 0 2d"
366
+ text += ' 00000000000000000000000000000000 A' if type == 'NSEC3'
367
+ record = Dnsruby::RR.create(text)
368
+ message = Dnsruby::Message.new
369
+ message.add_answer(record)
370
+ decoded = Dnsruby::Message.decode(message.encode).answer.first
371
+ assert_equal('2d', decoded.salt)
372
+ assert_equal(1, decoded.salt_length)
373
+ end
374
+ end
375
+
376
+ def test_validation_fast_path_leaves_dlv_eligibility_to_full_policy
377
+ private_key = OpenSSL::PKey::RSA.generate(1024)
378
+ key = rsa_dnskey(private_key, 8)
379
+ @verifier.add_trust_anchor(key)
380
+ records = signed_rsa('www.example. 60 IN A 192.0.2.1', key, private_key)
381
+ message = Dnsruby::Message.new('www.example.', 'A')
382
+ records.each { |rr| message.add_answer(rr) }
383
+ with_public_verifier(@verifier) do
384
+ Dnsruby::Dnssec.class_variable_set(:@@dlv_verifier, @verifier)
385
+ Dnsruby::Dnssec.class_variable_set(:@@anchor_verifier,
386
+ Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR))
387
+ Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::LOCAL_ANCHORS_THEN_ROOT
388
+ failure = ->(*) { raise Dnsruby::VerifyError, 'primary trust path failed' }
389
+ Dnsruby::Dnssec.stub(:validate_with_anchors, failure) do
390
+ Dnsruby::Dnssec.stub(:validate_with_root, failure) do
391
+ assert_raises(Dnsruby::VerifyError) { Dnsruby::Dnssec.validate(message) }
392
+ end
393
+ end
394
+ assert(Dnsruby::Dnssec.verify(records))
395
+ end
396
+ end
397
+
398
+ private
399
+
400
+ def signed_rsa(record, key, private_key)
401
+ record = Dnsruby::RR.create(record) if record.is_a?(String)
402
+ signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: record.ttl,
403
+ type_covered: record.type, algorithm: key.algorithm, labels: record.name.labels.length,
404
+ original_ttl: record.ttl, expiration: Time.now.to_i + 600,
405
+ inception: Time.now.to_i - 60, key_tag: key.key_tag, signers_name: key.name)
406
+ data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
407
+ digest = [5, 7].include?(key.algorithm.code) ? OpenSSL::Digest::SHA1.new : OpenSSL::Digest::SHA256.new
408
+ signature.signature = private_key.sign(digest, signature.sig_data + data)
409
+ Dnsruby::RRSet.new([record, signature])
410
+ end
411
+ end
412
+
413
+ require_relative 'tc_dnssec_denial_proofs'
414
+ class DnssecDenialProofsTest
415
+ include V2PublicDnssecHelper
416
+
417
+ def test_public_validation_full_policy_retains_cached_opt_out_proof_lifetime
418
+ message = response('missing.collision.invalid.', 'AAAA', nxdomain: true)
419
+ add_chain(message, flags: 1)
420
+ with_public_verifier(@verifier) do
421
+ assert_equal(:insecure, @verifier.verify_for_validation(message))
422
+ cached_expiry = message.instance_variable_get(:@dnsruby_authenticated_expiration)
423
+ full_validation = @verifier.method(:validate)
424
+ policy_calls = 0
425
+ validate = lambda do |*args|
426
+ policy_calls += 1
427
+ full_validation.call(*args)
428
+ end
429
+ @verifier.stub(:follow_chain, Dnsruby::RRSet.new([@key])) do
430
+ @verifier.stub(:validate, validate) do
431
+ assert(Dnsruby::Dnssec.validate(message))
432
+ end
433
+ end
434
+ assert_equal(1, policy_calls)
435
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
436
+ expiry = message.instance_variable_get(:@dnsruby_authenticated_expiration)
437
+ assert(expiry)
438
+ assert_equal(cached_expiry, expiry)
439
+ assert_operator(expiry, :<=, Time.now.to_i + 300)
440
+ end
441
+ end
442
+ end