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,405 @@
1
+ require_relative 'tc_key_cache_identity'
2
+
3
+ class ReleaseDnssecCompletionTest < Minitest::Test
4
+ def setup
5
+ @now = Time.now.to_i
6
+ @verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
7
+ end
8
+
9
+ def test_ds_owner_equivalence_and_configured_relative_anchor
10
+ private_key = OpenSSL::PKey::EC.new(KeyCacheIdentityTest::PRIVATE_KEYS.first)
11
+ public_bytes = private_key.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
12
+ key = Dnsruby::RR.create("Example.Invalid. 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(public_bytes)}")
13
+ ds = Dnsruby::RR::DS.from_key(key, 2)
14
+ ds.name = 'example.invalid'
15
+ assert(ds.check_key(key))
16
+ @verifier.add_trust_anchor(ds)
17
+ signature = signature_for(key, key)
18
+ der = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(canonical_data(key, signature)))
19
+ signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
20
+ assert(@verifier.verify(Dnsruby::RRSet.new([key, signature])))
21
+ other_owner = key.clone
22
+ other_owner.name = 'other.invalid.'
23
+ refute(ds.check_key(other_owner))
24
+ other_class = key.clone
25
+ other_class.klass = Dnsruby::Classes.CH
26
+ refute(ds.check_key(other_class))
27
+ end
28
+
29
+ def test_ed25519_verification_and_public_operational_validation
30
+ key, private_key = ed25519_key
31
+ record = Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1')
32
+ records = eddsa_signed(record, key, private_key)
33
+ assert_equal('ED25519', key.algorithm.to_s)
34
+ assert_equal(256, key.key_length)
35
+ assert_equal(32, key.key.bytesize)
36
+ assert_equal(64, records.sigs.first.signature.bytesize)
37
+ assert(@verifier.verify_rrset(records, key))
38
+ @verifier.add_trust_anchor(key)
39
+ message = Dnsruby::Message.new(record.name, 'A')
40
+ records.each { |rr| message.add_answer(rr) }
41
+ with_verifiers(anchor: @verifier) do
42
+ Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
43
+ assert(Dnsruby::Dnssec.validate(message))
44
+ assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
45
+ end
46
+ end
47
+
48
+ def test_ed25519_matches_published_dnssec_examples
49
+ skip 'OpenSSL provider does not support Ed25519' unless Dnsruby::RR::DNSKEY.ed25519_supported?
50
+ # RFC 8080 section 6.1, corrected by verified erratum 4935:
51
+ # https://www.rfc-editor.org/rfc/inline-errata/rfc8080.html
52
+ vectors = [
53
+ ['l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=', 3613,
54
+ '3aa5ab37efce57f737fc1627013fee07bdf241bd10f3b1964ab55c78e79a304b',
55
+ 'oL9krJun7xfBOIWcGHi7mag5/hdZrKWw15jPGrHpjQeRAvTdszaPD+QLs3fx8A4M3e23mRZ9VrbpMngwcrqNAg=='],
56
+ ['zPnZ/QwEe7S8C5SPz2OfS5RR40ATk2/rYnE9xHIEijs=', 35217,
57
+ '401781b934e392de492ec77ae2e15d70f6575a1c0bc59c5275c04ebe80c6614c',
58
+ 'zXQ0bkYgQTEFyfLyi9QoiY6D8ZdYo4wyUhVioYZXFdT410QPRITQSqJSnzQoSm5poJ7gD7AQR0O7KuI5k2pcBg==']
59
+ ]
60
+ Time.stub(:now, Time.at(1_439_000_000)) do
61
+ record = Dnsruby::RR.create('example.com. 3600 IN MX 10 mail.example.com.')
62
+ vectors.each do |public_key, tag, digest, signature|
63
+ key = Dnsruby::RR.create("example.com. 3600 IN DNSKEY 257 3 15 #{public_key}")
64
+ ds = Dnsruby::RR.create("example.com. 3600 IN DS #{tag} 15 2 #{digest}")
65
+ sig = Dnsruby::RR.create("example.com. 3600 IN RRSIG MX 15 2 3600 1440021600 1438207200 #{tag} example.com. #{signature}")
66
+ assert_equal(tag, key.key_tag)
67
+ assert(ds.check_key(key))
68
+ assert(@verifier.verify_rrset(Dnsruby::RRSet.new([record, sig]), key))
69
+ end
70
+ end
71
+ end
72
+
73
+ def test_ed25519_obeys_signature_time_identity_and_budget_checks
74
+ key, private_key = ed25519_key
75
+ record = Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1')
76
+ expired = eddsa_signed(record, key, private_key, expiration: @now - 1)
77
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(expired, key) }
78
+ other_owner = key.clone
79
+ other_owner.name = 'other.invalid.'
80
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(eddsa_signed(record, key, private_key), other_owner) }
81
+ records = eddsa_signed(record, key, private_key)
82
+ limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 0)
83
+ budget = Dnsruby::ValidationBudget.new(limits: limits)
84
+ assert_raises(Dnsruby::ValidationLimitError) do
85
+ Dnsruby::ValidationBudget.with(budget) { @verifier.verify_rrset(records, key) }
86
+ end
87
+ end
88
+
89
+ def test_ed25519_wire_sizes_and_changed_signatures_are_rejected
90
+ key, private_key = ed25519_key
91
+ assert_raises(ArgumentError) do
92
+ Dnsruby::RR.create("example.invalid. 300 IN DNSKEY 257 3 15 #{Base64.strict_encode64(key.key.byteslice(0, 31))}")
93
+ end
94
+ record = Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1')
95
+ records = eddsa_signed(record, key, private_key)
96
+ records.sigs.first.signature = records.sigs.first.signature.byteslice(0, 63)
97
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, key) }
98
+ records = eddsa_signed(record, key, private_key)
99
+ records.sigs.first.signature = private_key.sign(nil, 'different ordinary message')
100
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, key) }
101
+ end
102
+
103
+ def test_ed25519_unavailable_provider_uses_unsupported_algorithm_policy
104
+ key, private_key = ed25519_key
105
+ ds = Dnsruby::RR::DS.from_key(key, 2)
106
+ records = eddsa_signed(Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1'), key, private_key)
107
+ assert(@verifier.send(:supported_ds?, ds))
108
+ Dnsruby::RR::DNSKEY.stub(:ed25519_supported?, false) do
109
+ refute(@verifier.send(:supported_ds?, ds))
110
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, key) }
111
+ end
112
+ end
113
+
114
+ def test_ed25519_old_openssl_binding_is_reported_as_unsupported
115
+ klass = Dnsruby::RR::DNSKEY
116
+ saved = klass.instance_variable_get(:@ed25519_supported)
117
+ existed = klass.instance_variable_defined?(:@ed25519_supported)
118
+ klass.remove_instance_variable(:@ed25519_supported) if existed
119
+ old_key = Object.new
120
+ old_key.define_singleton_method(:verify) { |*| raise TypeError, 'no implicit conversion of nil into String' }
121
+ OpenSSL::PKey.stub(:read, old_key) { refute(klass.ed25519_supported?) }
122
+ ensure
123
+ if existed
124
+ klass.instance_variable_set(:@ed25519_supported, saved)
125
+ elsif klass.instance_variable_defined?(:@ed25519_supported)
126
+ klass.remove_instance_variable(:@ed25519_supported)
127
+ end
128
+ end
129
+
130
+ def test_ed448_verification_and_public_operational_validation
131
+ key, private_key = ed448_key
132
+ record = Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1')
133
+ records = eddsa_signed(record, key, private_key)
134
+ assert_equal('ED448', key.algorithm.to_s)
135
+ assert_equal(448, key.key_length)
136
+ assert_equal(57, key.key.bytesize)
137
+ assert_equal(114, records.sigs.first.signature.bytesize)
138
+ assert(@verifier.verify_rrset(records, key))
139
+ @verifier.add_trust_anchor(key)
140
+ message = Dnsruby::Message.new(record.name, 'A')
141
+ records.each { |rr| message.add_answer(rr) }
142
+ with_verifiers(anchor: @verifier) do
143
+ Dnsruby::Dnssec.validation_policy = Dnsruby::Dnssec::ValidationPolicy::ALWAYS_LOCAL_ANCHORS_ONLY
144
+ assert(Dnsruby::Dnssec.validate(message))
145
+ assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
146
+ end
147
+ end
148
+
149
+ def test_ed448_matches_published_dnssec_examples
150
+ skip 'OpenSSL provider does not support Ed448' unless Dnsruby::RR::DNSKEY.ed448_supported?
151
+ # RFC 8080 section 6.2, corrected by verified erratum 4935:
152
+ # https://www.rfc-editor.org/rfc/inline-errata/rfc8080.html
153
+ vectors = [
154
+ ['3kgROaDjrh0H2iuixWBrc8g2EpBBLCdGzHmn+G2MpTPhpj/OiBVHHSfPodx1FYYUcJKm1MDpJtIA', 9713,
155
+ '6ccf18d5bc5d7fc2fceb1d59d17321402f2aa8d368048db93dd811f5cb2b19c7',
156
+ '3cPAHkmlnxcDHMyg7vFC34l0blBhuG1qpwLmjInI8w1CMB29FkEAIJUA0amxWndkmnBZ6SKiwZSAxGILn/NBtOXft0+Gj7FSvOKxE/07+4RQvE581N3Aj/JtIyaiYVdnYtyMWbSNyGEY2213WKsJlwEA'],
157
+ ['kkreGWoccSDmUBGAe7+zsbG6ZAFQp+syPmYUurBRQc3tDjeMCJcVMRDmgcNLp5HlHAMy12VoISsA', 38353,
158
+ '645ff078b3568f5852b70cb60e8e696cc77b75bfaaffc118cf79cbda1ba28af4',
159
+ 'E1/oLjSGIbmLny/4fcgM1z4oL6aqo+izT3urCyHyvEp4Sp8Syg1eI+lJ57CSnZqjJP41O/9l4m0AsQ4f7qI1gVnML8vWWiyW2KXhT9kuAICUSxv5OWbf81Rq7Yu60npabODB0QFPb/rkW3kUZmQ0YQUA']
160
+ ]
161
+ Time.stub(:now, Time.at(1_439_000_000)) do
162
+ record = Dnsruby::RR.create('example.com. 3600 IN MX 10 mail.example.com.')
163
+ vectors.each do |public_key, tag, digest, signature|
164
+ key = Dnsruby::RR.create("example.com. 3600 IN DNSKEY 257 3 16 #{public_key}")
165
+ ds = Dnsruby::RR.create("example.com. 3600 IN DS #{tag} 16 2 #{digest}")
166
+ sig = Dnsruby::RR.create("example.com. 3600 IN RRSIG MX 16 2 3600 1440021600 1438207200 #{tag} example.com. #{signature}")
167
+ assert_equal(tag, key.key_tag)
168
+ assert(ds.check_key(key))
169
+ assert(@verifier.verify_rrset(Dnsruby::RRSet.new([record, sig]), key))
170
+ end
171
+ end
172
+ end
173
+
174
+ def test_ed448_wire_sizes_and_unavailable_provider_policy
175
+ key, private_key = ed448_key
176
+ assert_raises(ArgumentError) do
177
+ Dnsruby::RR.create("example.invalid. 300 IN DNSKEY 257 3 16 #{Base64.strict_encode64(key.key.byteslice(0, 56))}")
178
+ end
179
+ record = Dnsruby::RR.create('www.example.invalid. 300 IN A 192.0.2.1')
180
+ records = eddsa_signed(record, key, private_key)
181
+ records.sigs.first.signature = records.sigs.first.signature.byteslice(0, 113)
182
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, key) }
183
+ ds = Dnsruby::RR::DS.from_key(key, 2)
184
+ assert(@verifier.send(:supported_ds?, ds))
185
+ Dnsruby::RR::DNSKEY.stub(:ed448_supported?, false) do
186
+ refute(@verifier.send(:supported_ds?, ds))
187
+ assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(eddsa_signed(record, key, private_key), key) }
188
+ end
189
+ end
190
+
191
+ def test_ed448_old_openssl_binding_is_reported_as_unsupported
192
+ klass = Dnsruby::RR::DNSKEY
193
+ saved = klass.instance_variable_get(:@ed448_supported)
194
+ existed = klass.instance_variable_defined?(:@ed448_supported)
195
+ klass.remove_instance_variable(:@ed448_supported) if existed
196
+ old_key = Object.new
197
+ old_key.define_singleton_method(:verify) { |*| raise TypeError, 'no implicit conversion of nil into String' }
198
+ OpenSSL::PKey.stub(:read, old_key) { refute(klass.ed448_supported?) }
199
+ ensure
200
+ if existed
201
+ klass.instance_variable_set(:@ed448_supported, saved)
202
+ elsif klass.instance_variable_defined?(:@ed448_supported)
203
+ klass.remove_instance_variable(:@ed448_supported)
204
+ end
205
+ end
206
+
207
+ def test_all_root_policies_preserve_authenticated_insecure_negative_lifetime
208
+ policies = Dnsruby::Dnssec::ValidationPolicy
209
+ [policies::ALWAYS_ROOT_ONLY, policies::LOCAL_ANCHORS_THEN_ROOT, policies::ROOT_THEN_LOCAL_ANCHORS].each do |policy|
210
+ root = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
211
+ root.add_trust_anchor(Dnsruby::RR.create(". 300 IN DS 12345 13 2 #{'AB' * 32}"))
212
+ deadline = @now + 7
213
+ root.define_singleton_method(:follow_chain) do |*|
214
+ send(:trust_operation)[:insecurity_expirations] << deadline
215
+ nil
216
+ end
217
+ query = Dnsruby::Message.new('absent.example.invalid.', 'A')
218
+ query.do_validation = true
219
+ query.header.cd = true
220
+ message = Dnsruby::Message.new('absent.example.invalid.', 'A')
221
+ message.header.qr = true
222
+ message.header.rcode = Dnsruby::RCode.NXDOMAIN
223
+ message.add_authority(Dnsruby::RR.create('example.invalid. 300 IN SOA ns.example.invalid. hostmaster.example.invalid. 1 3600 600 86400 300'))
224
+ with_verifiers(root: root) do
225
+ Dnsruby::Dnssec.validation_policy = policy
226
+ clock = @now
227
+ Time.stub(:now, -> { Time.at(clock) }) do
228
+ assert(Dnsruby::Dnssec.validate_with_query(query, message))
229
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
230
+ assert_equal(deadline, message.instance_variable_get(:@dnsruby_authenticated_expiration), policy.to_s)
231
+ cache = Dnsruby::Cache::ResolutionCache.new
232
+ cache.add(query, :test_scope, message, Dnsruby::NXDomain.new)
233
+ refute_nil(cache.find(query, :test_scope), policy.to_s)
234
+ clock = deadline
235
+ assert_nil(cache.find(query, :test_scope))
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ def test_unanchored_validation_does_not_reuse_previous_insecurity_proof
242
+ message = Dnsruby::Message.new('absent.example.invalid.', 'A')
243
+ message.instance_variable_set(:@dnsruby_authenticated_expiration, @now + 300)
244
+ assert(@verifier.validate(message, nil))
245
+ assert_nil(message.instance_variable_get(:@dnsruby_authenticated_expiration))
246
+ end
247
+
248
+ def test_hints_changes_replace_shared_recursor_and_close_bootstrap
249
+ klass = Dnsruby::SingleVerifier
250
+ saved = klass.instance_variables.to_h { |name| [name, klass.instance_variable_get(name)] }
251
+ boots, hints, resets = [], [], []
252
+ bootstrap = Struct.new(:closed) { def close; self.closed = true; end }
253
+ recursor = Struct.new(:dnssec)
254
+ Dnsruby::Recursor.stub(:reset_hints, -> { resets << true }) do
255
+ Dnsruby::Recursor.stub(:set_hints, ->(value, _) { hints << value }) do
256
+ Dnsruby::Recursor.stub(:new, -> { recursor.new }) do
257
+ Dnsruby::Resolver.stub(:new, -> { boots << bootstrap.new; boots.last }) do
258
+ supplied = [+'192.0.2.1']
259
+ Dnsruby::Dnssec.set_hints(supplied)
260
+ supplied.first.replace('192.0.2.9')
261
+ first = @verifier.get_recursor
262
+ assert_same(first, @verifier.get_recursor)
263
+ Dnsruby::Dnssec.set_hints(['192.0.2.2'])
264
+ second = @verifier.get_recursor
265
+ refute_same(first, second)
266
+ assert(second.dnssec)
267
+ assert_equal([['192.0.2.1'], ['192.0.2.2']], hints)
268
+ assert(boots.all?(&:closed))
269
+ klass.set_recursor_hints(nil)
270
+ refute_same(second, @verifier.get_recursor)
271
+ assert_equal(3, resets.length)
272
+ end
273
+ end
274
+ end
275
+ end
276
+ ensure
277
+ (klass.instance_variables - saved.keys).each { |name| klass.remove_instance_variable(name) } if saved
278
+ saved&.each { |name, value| klass.instance_variable_set(name, value) }
279
+ end
280
+
281
+ def test_dnssec_reset_discards_shared_recursor_hints
282
+ saved = Dnsruby::Dnssec.class_variables.to_h { |name| [name, Dnsruby::Dnssec.class_variable_get(name)] }
283
+ calls = []
284
+ Dnsruby::SingleVerifier.stub(:set_recursor_hints, ->(hints) { calls << hints }) do
285
+ Dnsruby::Dnssec.reset
286
+ assert_equal([nil], calls)
287
+ end
288
+ ensure
289
+ replacement = Dnsruby::Dnssec.default_resolver
290
+ replacement.close unless replacement.equal?(saved[:@@default_resolver])
291
+ saved.each { |name, value| Dnsruby::Dnssec.class_variable_set(name, value) }
292
+ end
293
+
294
+ private
295
+
296
+ def ed25519_key
297
+ skip 'OpenSSL provider does not support Ed25519' unless Dnsruby::RR::DNSKEY.ed25519_supported?
298
+ # RFC 8032 section 7.1, test 1. Import its fixed private seed as RFC 8410 PKCS#8.
299
+ seed = ['9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'].pack('H*')
300
+ der = OpenSSL::ASN1::Sequence.new([
301
+ OpenSSL::ASN1::Integer.new(0),
302
+ OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::ObjectId.new('1.3.101.112')]),
303
+ OpenSSL::ASN1::OctetString.new(OpenSSL::ASN1::OctetString.new(seed).to_der)
304
+ ]).to_der
305
+ private_key = OpenSSL::PKey.read(der)
306
+ public_bytes = OpenSSL::ASN1.decode(private_key.public_to_der).value.last.value
307
+ assert_equal('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a', public_bytes.unpack1('H*'))
308
+ [Dnsruby::RR.create("example.invalid. 300 IN DNSKEY 257 3 ED25519 #{Base64.strict_encode64(public_bytes)}"), private_key]
309
+ end
310
+
311
+ def ed448_key
312
+ skip 'OpenSSL provider does not support Ed448' unless Dnsruby::RR::DNSKEY.ed448_supported?
313
+ # RFC 8032 section 7.4, empty-message vector, imported as RFC 8410 PKCS#8.
314
+ seed = ['6c82a562cb808d10d632be89c8513ebf' \
315
+ '6c929f34ddfa8c9f63c9960ef6e348a3' \
316
+ '528c8a3fcc2f044e39a3fc5b94492f8f' \
317
+ '032e7549a20098f95b'].pack('H*')
318
+ der = OpenSSL::ASN1::Sequence.new([
319
+ OpenSSL::ASN1::Integer.new(0),
320
+ OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::ObjectId.new('1.3.101.113')]),
321
+ OpenSSL::ASN1::OctetString.new(OpenSSL::ASN1::OctetString.new(seed).to_der)
322
+ ]).to_der
323
+ private_key = OpenSSL::PKey.read(der)
324
+ public_bytes = OpenSSL::ASN1.decode(private_key.public_to_der).value.last.value
325
+ [Dnsruby::RR.create("example.invalid. 300 IN DNSKEY 257 3 ED448 #{Base64.strict_encode64(public_bytes)}"), private_key]
326
+ end
327
+
328
+ def signature_for(record, key, expiration: @now + 300)
329
+ Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: record.ttl,
330
+ type_covered: record.type, algorithm: key.algorithm, labels: record.name.labels.length,
331
+ original_ttl: record.ttl, expiration: expiration, inception: @now - 60,
332
+ key_tag: key.key_tag, signers_name: key.name)
333
+ end
334
+
335
+ def canonical_data(record, signature)
336
+ signature.sig_data + Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
337
+ end
338
+
339
+ def eddsa_signed(record, key, private_key, **options)
340
+ signature = signature_for(record, key, **options)
341
+ signature.signature = private_key.sign(nil, canonical_data(record, signature))
342
+ Dnsruby::RRSet.new([record, signature])
343
+ end
344
+
345
+ def with_verifiers(anchor: nil, root: nil)
346
+ names = %i[@@anchor_verifier @@root_verifier @@dlv_verifier @@validation_policy]
347
+ saved = names.map { |name| Dnsruby::Dnssec.class_variable_get(name) }
348
+ values = [anchor, root, nil].map { |verifier| verifier || Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR) }
349
+ names.first(3).zip(values).each { |name, value| Dnsruby::Dnssec.class_variable_set(name, value) }
350
+ yield
351
+ ensure
352
+ names.zip(saved).each { |name, value| Dnsruby::Dnssec.class_variable_set(name, value) }
353
+ end
354
+ end
355
+
356
+ class KeyCacheIdentityTest
357
+ def test_changed_anchor_owner_does_not_match_the_old_name_index
358
+ [@a.clone, Dnsruby::RR::DS.from_key(@a, 2)].each do |anchor|
359
+ @verifier.clear_trust_anchors
360
+ @verifier.add_trust_anchor(anchor)
361
+ assert(@verifier.find_closest_anchor_for('www.collision.invalid.'))
362
+ anchor.name = 'moved.invalid.'
363
+ refute(@verifier.find_closest_anchor_for('www.collision.invalid.'))
364
+ end
365
+ end
366
+
367
+ def test_owner_indexes_remove_evicted_and_expired_keys_and_ds_sets
368
+ clock = @now
369
+ Time.stub(:now, -> { clock }) do
370
+ keys = Dnsruby::KeyCache.new(max_keys: 1)
371
+ first, second = @a, @a.clone
372
+ second.name = 'second.invalid.'
373
+ keys.add_key_with_expiration(first, @now.to_i + 60)
374
+ assert_equal([first], keys.keys_for_name(Dnsruby::Name.create('COLLISION.INVALID')))
375
+ keys.add_key_with_expiration(second, @now.to_i + 60)
376
+ assert_empty(keys.keys_for_name(first.name))
377
+ assert_equal([second], keys.keys_for_name(second.name))
378
+ keys.delete(second)
379
+ assert_empty(keys.keys_for_name(second.name))
380
+ keys.add_key_with_expiration(second, @now.to_i + 60)
381
+ sets = Dnsruby::DSCache.new(max_records: 1)
382
+ first_ds, second_ds = [first, second].map { |key| Dnsruby::RRSet.new([Dnsruby::RR::DS.from_key(key, 2)]) }
383
+ sets.add(first_ds, @now.to_i + 60)
384
+ assert_equal([first_ds], sets.rrsets_for_name(Dnsruby::Name.create('COLLISION.INVALID')))
385
+ sets.add(second_ds, @now.to_i + 60)
386
+ assert_empty(sets.rrsets_for_name(first.name))
387
+ assert_equal([second_ds], sets.rrsets_for_name(second.name))
388
+ clock = @now + 60
389
+ assert_empty(keys.keys_for_name(second.name))
390
+ assert_empty(sets.rrsets_for_name(second.name))
391
+ end
392
+ end
393
+
394
+ def test_closest_anchor_does_not_visit_unrelated_learned_provenance
395
+ 24.times do |index|
396
+ key = @a.clone
397
+ key.name = "zone#{index}.invalid."
398
+ @verifier.add_trusted_key(Dnsruby::RRSet.new([key]), @now.to_i + 300)
399
+ end
400
+ @verifier.add_trust_anchor(@a)
401
+ @verifier.stub(:usable_cached_key?, ->(_) { flunk('unrelated provenance should not be consulted') }) do
402
+ assert_equal(@a, @verifier.find_closest_anchor_for('www.COLLISION.invalid'))
403
+ end
404
+ end
405
+ end