dnsruby 1.74.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
data/test/tc_header.rb
CHANGED
|
@@ -15,11 +15,37 @@
|
|
|
15
15
|
# ++
|
|
16
16
|
|
|
17
17
|
require_relative 'spec_helper'
|
|
18
|
+
require 'minitest/mock'
|
|
18
19
|
|
|
19
20
|
class TestHeader < Minitest::Test
|
|
20
21
|
|
|
21
22
|
include Dnsruby
|
|
22
23
|
|
|
24
|
+
def test_generated_ids_use_secure_randomness_and_include_both_endpoints
|
|
25
|
+
[0, 65_535].each do |id|
|
|
26
|
+
generator = lambda do |range|
|
|
27
|
+
assert_equal(65_536, range)
|
|
28
|
+
id
|
|
29
|
+
end
|
|
30
|
+
SecureRandom.stub(:random_number, generator) do
|
|
31
|
+
header = Header.new
|
|
32
|
+
assert_equal(id, header.id)
|
|
33
|
+
assert_equal(id, header.data.unpack1('n'))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_header_decoding_does_not_generate_an_unused_id
|
|
39
|
+
bytes = [65_535, 0x8180, 1, 2, 3, 4].pack('n6')
|
|
40
|
+
SecureRandom.stub(:random_number, ->(*) { flunk('Decoding must retain the wire ID') }) do
|
|
41
|
+
header = Header.new_from_data(bytes)
|
|
42
|
+
assert_equal(65_535, header.id)
|
|
43
|
+
assert_equal(bytes, header.data)
|
|
44
|
+
updated = Header.decrement_arcount_encoded(bytes.dup + 'payload')
|
|
45
|
+
assert_equal([65_535, 0x8180, 1, 2, 3, 3].pack('n6') + 'payload', updated)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
23
49
|
def test_header
|
|
24
50
|
header = Header.new();
|
|
25
51
|
assert(header, "new() returned something")
|
data/test/tc_ipv6.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Tests for the RFC 5952 text representation of IPv6 addresses
|
|
4
|
+
class TestIPv6 < Minitest::Test
|
|
5
|
+
|
|
6
|
+
include Dnsruby
|
|
7
|
+
|
|
8
|
+
def test_default_text_preserves_v1_74_formatting
|
|
9
|
+
{
|
|
10
|
+
'2001:db8::ab' => '2001:DB8::AB',
|
|
11
|
+
'1:0:0:1:0:0:0:1' => '1::1:0:0:0:1',
|
|
12
|
+
'2001:db8:0:1:1:1:1:1' => '2001:DB8::1:1:1:1:1',
|
|
13
|
+
'::ffff:192.0.2.1' => '::FFFF:C000:201',
|
|
14
|
+
'::' => '::',
|
|
15
|
+
'::1' => '::1',
|
|
16
|
+
'1:2:3:4:5:6:7:0' => '1:2:3:4:5:6:7::'
|
|
17
|
+
}.each do |input, expected|
|
|
18
|
+
address = IPv6.create(input)
|
|
19
|
+
assert_equal(expected, address.to_s)
|
|
20
|
+
assert_equal("#<Dnsruby::IPv6 #{expected}>", address.inspect)
|
|
21
|
+
assert_equal(address, IPv6.create(address.to_s))
|
|
22
|
+
rr = RR.create("example. 60 IN AAAA #{input}")
|
|
23
|
+
assert_equal(expected, rr.address.to_s)
|
|
24
|
+
assert_equal(expected, rr.rdata_to_string)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_compression_requires_at_least_one_omitted_field
|
|
29
|
+
%w[
|
|
30
|
+
1:2:3:4:5:6:7::8 ::1:2:3:4:5:6:7:8 1:2:3:4:5:6:7:8::
|
|
31
|
+
1:2:3:4:5:6::192.0.2.1 ::1:2:3:4:5:6:192.0.2.1
|
|
32
|
+
].each do |address|
|
|
33
|
+
assert_raises(ArgumentError, address) { IPv6.create(address) }
|
|
34
|
+
end
|
|
35
|
+
assert_equal('1:2:3:4:5:6:0:8', IPv6.create('1:2:3:4:5:6::8').to_s_canonical)
|
|
36
|
+
assert_equal('1:2:3:4:5:0:c000:201', IPv6.create('1:2:3:4:5::192.0.2.1').to_s_canonical)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# RFC 5952 section 4.1: leading zeros in a field are suppressed
|
|
40
|
+
def test_leading_zeros_suppressed
|
|
41
|
+
assert_equal('2001:db8::1', IPv6.create('2001:0db8:0000:0000:0000:0000:0000:0001').to_s_canonical)
|
|
42
|
+
assert_equal('2001:db8:0:1:1:1:1:1', IPv6.create('2001:0db8:0000:0001:0001:0001:0001:0001').to_s_canonical)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# RFC 5952 section 4.2.1: a run of all-zero fields is replaced by '::'
|
|
46
|
+
def test_zero_run_shortened
|
|
47
|
+
assert_equal('::', IPv6.create('0:0:0:0:0:0:0:0').to_s_canonical)
|
|
48
|
+
assert_equal('::1', IPv6.create('0:0:0:0:0:0:0:1').to_s_canonical)
|
|
49
|
+
assert_equal('1::', IPv6.create('1:0:0:0:0:0:0:0').to_s_canonical)
|
|
50
|
+
assert_equal('2001:db8::1', IPv6.create('2001:db8:0:0:0:0:0:1').to_s_canonical)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# RFC 5952 section 4.2.2: '::' must not shorten a single all-zero field
|
|
54
|
+
def test_single_zero_field_not_shortened
|
|
55
|
+
assert_equal('2001:db8:0:1:1:1:1:1', IPv6.create('2001:db8:0:1:1:1:1:1').to_s_canonical)
|
|
56
|
+
assert_equal('1:2:3:4:5:6:0:8', IPv6.create('1:2:3:4:5:6:0:8').to_s_canonical)
|
|
57
|
+
assert_equal('0:1:2:3:4:5:6:7', IPv6.create('0:1:2:3:4:5:6:7').to_s_canonical)
|
|
58
|
+
assert_equal('1:2:3:4:5:6:7:0', IPv6.create('1:2:3:4:5:6:7:0').to_s_canonical)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# RFC 5952 section 4.2.3: the longest run is shortened. If there's a tie,
|
|
62
|
+
# shorten the first run.
|
|
63
|
+
def test_longest_zero_run_shortened
|
|
64
|
+
assert_equal('1:0:0:1::1', IPv6.create('1:0:0:1:0:0:0:1').to_s_canonical)
|
|
65
|
+
assert_equal('1::1:0:0:1:1', IPv6.create('1:0:0:1:0:0:1:1').to_s_canonical)
|
|
66
|
+
assert_equal('2001:db8::1:0:0:1', IPv6.create('2001:db8:0:0:1:0:0:1').to_s_canonical)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# RFC 5952 section 4.3: downcase hexadecimal digits
|
|
70
|
+
def test_lowercase_hex
|
|
71
|
+
assert_equal('2001:db8::ab', IPv6.create('2001:0DB8::AB').to_s_canonical)
|
|
72
|
+
assert_equal('fe80::abcd:ef01', IPv6.create('FE80::ABCD:EF01').to_s_canonical)
|
|
73
|
+
assert_equal('2606:4700:20::681a:205', IPv6.create('2606:4700:20::681A:205').to_s_canonical)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# RFC 5952 section 5: an embedded IPv4 address under the well-known
|
|
77
|
+
# ::ffff:0:0/96 prefix uses mixed notation
|
|
78
|
+
def test_ipv4_mapped_mixed_notation
|
|
79
|
+
assert_equal('::ffff:192.0.2.1', IPv6.create('::ffff:192.0.2.1').to_s_canonical)
|
|
80
|
+
assert_equal('::ffff:192.0.2.1', IPv6.create('0:0:0:0:0:ffff:c000:201').to_s_canonical)
|
|
81
|
+
assert_equal('::ffff:0.0.0.0', IPv6.create('::ffff:0:0').to_s_canonical)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Only ::ffff:0:0/96 gets the mixed notation. IPv4-compatible addresses
|
|
85
|
+
# (::/96) are deprecated by RFC 4291 section 2.5.5.1, so a dotted quad there
|
|
86
|
+
# would imply a semantic that no longer exists - they keep the hexadecimal
|
|
87
|
+
# form. Note this is a deliberate departure from IPAddr and inet_ntop, which
|
|
88
|
+
# write ::a:b as ::0.10.0.11.
|
|
89
|
+
def test_non_mapped_addresses_stay_hexadecimal
|
|
90
|
+
assert_equal('::', IPv6.create('::').to_s_canonical)
|
|
91
|
+
assert_equal('::1', IPv6.create('::1').to_s_canonical)
|
|
92
|
+
assert_equal('::102:304', IPv6.create('::1.2.3.4').to_s_canonical)
|
|
93
|
+
assert_equal('64:ff9b::102:304', IPv6.create('64:ff9b::1.2.3.4').to_s_canonical)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The mixed notation requires the whole ::ffff:0:0/96 prefix, not just 0xffff
|
|
97
|
+
# in the sixth field
|
|
98
|
+
def test_mixed_notation_requires_the_full_prefix
|
|
99
|
+
assert_equal('1::ffff:102:304', IPv6.create('1::ffff:1.2.3.4').to_s_canonical)
|
|
100
|
+
assert_equal('::1:ffff:102:304', IPv6.create('::1:ffff:1.2.3.4').to_s_canonical)
|
|
101
|
+
assert_equal('::fffe:102:304', IPv6.create('::fffe:1.2.3.4').to_s_canonical)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Whatever we emit must be readable back as the same address
|
|
105
|
+
def test_output_round_trips_through_create
|
|
106
|
+
[
|
|
107
|
+
'2001:0DB8:0:8002::2000:1',
|
|
108
|
+
'2001:db8:0:1:1:1:1:1',
|
|
109
|
+
'1:0:0:1:0:0:0:1',
|
|
110
|
+
'::ffff:192.0.2.1',
|
|
111
|
+
'::',
|
|
112
|
+
'::1',
|
|
113
|
+
'1::',
|
|
114
|
+
'fe80::1',
|
|
115
|
+
].each do |text|
|
|
116
|
+
address = IPv6.create(text)
|
|
117
|
+
assert_equal(address, IPv6.create(address.to_s_canonical), "#{text} did not survive canonical formatting / create")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
|
|
7
|
+
class KeyCacheIdentityTest < Minitest::Test
|
|
8
|
+
# Test-only keys with different public-key material but the same DNSKEY tag
|
|
9
|
+
# (48708 with flags 257, protocol 3 and algorithm 13). Fixed keys avoid a
|
|
10
|
+
# probabilistic collision search and make the cryptographic checks repeatable.
|
|
11
|
+
PRIVATE_KEYS = [<<~PEM, <<~PEM].freeze
|
|
12
|
+
-----BEGIN EC PRIVATE KEY-----
|
|
13
|
+
MHcCAQEEICU+8aMcoyz9AMcy/h7MbFCYLKxDS/EtQPgHaXSUhzu8oAoGCCqGSM49
|
|
14
|
+
AwEHoUQDQgAE2dAvoL88tZbfwgPEytuVr9EsFi79Hw3l5YtxTtu1QO2NIzjD8Gdb
|
|
15
|
+
WBuInfkty8P9k2zCOr7ngwuKEFCNHx5Dug==
|
|
16
|
+
-----END EC PRIVATE KEY-----
|
|
17
|
+
PEM
|
|
18
|
+
-----BEGIN EC PRIVATE KEY-----
|
|
19
|
+
MHcCAQEEIAL8ZvOFzy7oNwn/0NhgQ+hWu4UrvSJXK5nKPsAo5evFoAoGCCqGSM49
|
|
20
|
+
AwEHoUQDQgAE1fdv3FiCisktVXjdRZRD7QjneeVdiLSLjFOpp1pynxXBmw0a+1Tw
|
|
21
|
+
4LJ0KKrSuwVVv66IIXXWMRoyOxIfZsOVBA==
|
|
22
|
+
-----END EC PRIVATE KEY-----
|
|
23
|
+
PEM
|
|
24
|
+
|
|
25
|
+
DSA_PRIVATE_KEY = <<~PEM.freeze
|
|
26
|
+
-----BEGIN DSA PRIVATE KEY-----
|
|
27
|
+
MIIBvAIBAAKBgQD9x9CuGC3tbWplxT+2Zp030mynG3KnzyBPIKfYRx+TDprNHQH4
|
|
28
|
+
j/ptA/SPEPboIwO4QvQ52jsA94Gz9Frcwc/IxdavY6rS6L5N3y327ogD9Ry30jpB
|
|
29
|
+
kvaHtKjKlJjlnG1jzzj+fKoHImG2B+Be3AmmKuXw2f1wlMdO+FkcNPqdLwIVAOI9
|
|
30
|
+
73RMLRrptdT9VxtgmB2FZjrrAoGBAOG6M628TvLmfGPdlRUlsL6XvW01swv21+e/
|
|
31
|
+
n/wi6UIqmaQZrh0hOQ9PW97/po2mMViG79xcvcbzMshgxF7nNsGethGQ9zbK3ynd
|
|
32
|
+
gxonua6RvQa/6aGvSJ3Yx2MSeeVdyVGhz+kARxumg9lHBNftx6P29NGxqbYHcWhE
|
|
33
|
+
+59wwlQhAoGBAMFmlnzv86whfLgToGZKl9tl6LEMbvR7J9KQZ4czX+LTguMGprtQ
|
|
34
|
+
5WAU3oC9FLAPyUFyBhmSv2VVe5ob+07z/Ofpx3PtWAbZYlJdrgh7ZVtluVWewfGN
|
|
35
|
+
DqPy94Ncz3rORtR96NsGOmct1jmoZOp87KPKTuftb3bzSroLLn/6V1dtAhR77/9H
|
|
36
|
+
DOw8H1aQpmF7D4JVCnyExQ==
|
|
37
|
+
-----END DSA PRIVATE KEY-----
|
|
38
|
+
PEM
|
|
39
|
+
|
|
40
|
+
def setup
|
|
41
|
+
@now = Time.now
|
|
42
|
+
@private_keys = PRIVATE_KEYS.map { |pem| OpenSSL::PKey::EC.new(pem) }
|
|
43
|
+
@a, @b = @private_keys.map do |key|
|
|
44
|
+
bytes = key.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
|
|
45
|
+
Dnsruby::RR.create("collision.invalid. 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(bytes)}")
|
|
46
|
+
end
|
|
47
|
+
@cache = Dnsruby::KeyCache.new
|
|
48
|
+
@verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
49
|
+
@record = Dnsruby::RR.create('www.collision.invalid. 300 IN A 192.0.2.1')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_collision_storage_and_exact_deletion_ignore_ttl_and_owner_case
|
|
53
|
+
refute_equal(@a.key, @b.key)
|
|
54
|
+
assert_equal(@a.key_tag, @b.key_tag)
|
|
55
|
+
@cache.add_key_with_expiration(@a, @now.to_i + 60)
|
|
56
|
+
@cache.add_key_with_expiration(@b, @now.to_i + 120)
|
|
57
|
+
assert_equal([@a, @b], @cache.keys)
|
|
58
|
+
|
|
59
|
+
equivalent = Dnsruby::RR.create(@a.to_s)
|
|
60
|
+
equivalent.name = 'COLLISION.INVALID.'
|
|
61
|
+
equivalent.ttl += 1
|
|
62
|
+
assert_equal(@a, @cache.delete(equivalent))
|
|
63
|
+
assert_equal([@b], @cache.keys)
|
|
64
|
+
assert_nil(@cache.delete(equivalent))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_equivalent_additions_retain_longest_expiration
|
|
68
|
+
equivalent = Dnsruby::RR.create(@a.to_s)
|
|
69
|
+
equivalent.ttl += 1
|
|
70
|
+
@cache.add_key_with_expiration(@a, @now.to_i + 120)
|
|
71
|
+
@cache.add_key_with_expiration(equivalent, @now.to_i + 60)
|
|
72
|
+
Time.stub(:now, @now + 90) { assert_equal([@a], @cache.keys) }
|
|
73
|
+
@cache.add_key_with_expiration(equivalent, @now.to_i + 240)
|
|
74
|
+
Time.stub(:now, @now + 180) { assert_equal([equivalent], @cache.keys) }
|
|
75
|
+
Time.stub(:now, @now + 241) { assert_empty(@cache.keys) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_copy_retains_both_colliding_keys_and_their_individual_expirations
|
|
79
|
+
@cache.add_key_with_expiration(@a, @now.to_i + 60)
|
|
80
|
+
@cache.add_key_with_expiration(@b, @now.to_i + 120)
|
|
81
|
+
copy = Dnsruby::KeyCache.new(@cache)
|
|
82
|
+
assert_equal([[@now.to_i + 60, @a], [@now.to_i + 120, @b]], copy.keys_and_expirations)
|
|
83
|
+
copy.keys_and_expirations.first[0] = 0
|
|
84
|
+
@cache.delete(@b)
|
|
85
|
+
assert_equal([@a, @b], copy.keys)
|
|
86
|
+
Time.stub(:now, @now + 90) { assert_equal([@b], copy.keys) }
|
|
87
|
+
Time.stub(:now, @now + 121) { assert_empty(copy.keys) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_copy_omits_expired_keys
|
|
91
|
+
@cache.add_key_with_expiration(@a, @now.to_i - 1)
|
|
92
|
+
@cache.add_key_with_expiration(@b, @now.to_i + 120)
|
|
93
|
+
assert_equal([@b], Dnsruby::KeyCache.new(@cache).keys)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_other_rrset_signatures_are_not_changed_or_added_to_key_cache
|
|
97
|
+
records = signed(@record, @a, 0)
|
|
98
|
+
@cache.add_rrset(records)
|
|
99
|
+
assert_empty(@cache.keys)
|
|
100
|
+
assert_equal(Dnsruby::Types.A, records.sigs.first.type_covered)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_both_same_zone_colliding_keys_can_verify_in_either_order
|
|
104
|
+
[0, 1].each do |signer|
|
|
105
|
+
records = signed(@record, [@a, @b][signer], signer)
|
|
106
|
+
[[@a, @b], [@b, @a]].each do |keys|
|
|
107
|
+
assert(@verifier.verify_rrset(records, keys))
|
|
108
|
+
assert(@verifier.verify_rrset(records, Dnsruby::RRSet.new(keys)))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
[@a, @b].each { |key| @verifier.add_trust_anchor(key) }
|
|
112
|
+
assert(@verifier.verify_rrset(signed(@record, @a, 0)))
|
|
113
|
+
assert(@verifier.verify_rrset(signed(@record, @b, 1)))
|
|
114
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(signed(@record, @a, 0), @b) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_dnskey_signer_membership_uses_full_identity_and_keeps_candidate_fallback
|
|
118
|
+
# A valid self-signed keyset and a different ordinary key sharing its tag.
|
|
119
|
+
records = signed(@a, @a, 0)
|
|
120
|
+
refute_equal(Dnsruby::KeyCache.key_identity(@a), Dnsruby::KeyCache.key_identity(@b))
|
|
121
|
+
assert_equal(@a.key_tag, @b.key_tag)
|
|
122
|
+
attempted = []
|
|
123
|
+
original = @verifier.method(:verify_rrset_with_key)
|
|
124
|
+
@verifier.stub(:verify_rrset_with_key, lambda { |set, key, *args|
|
|
125
|
+
attempted << Dnsruby::KeyCache.key_identity(key)
|
|
126
|
+
original.call(set, key, *args)
|
|
127
|
+
}) do
|
|
128
|
+
assert(@verifier.verify(records, [@b, @a]))
|
|
129
|
+
assert_equal([Dnsruby::KeyCache.key_identity(@a)], attempted)
|
|
130
|
+
attempted.clear
|
|
131
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, @b) }
|
|
132
|
+
assert_empty(attempted)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_same_public_key_at_another_owner_cannot_verify
|
|
137
|
+
foreign_key = Dnsruby::RR.create(@a.to_s)
|
|
138
|
+
foreign_key.name = 'unrelated.invalid.'
|
|
139
|
+
records = signed(@record, @a, 0)
|
|
140
|
+
error = assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, foreign_key) }
|
|
141
|
+
assert_match(/Signing key not found/, error.message)
|
|
142
|
+
assert(@verifier.verify_rrset(records, [foreign_key, @a]))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_wire_decoded_malformed_collision_does_not_hide_valid_signing_key
|
|
146
|
+
malformed = Dnsruby::RR.create(@a.to_s)
|
|
147
|
+
# These bytes have the same key tag but are not a valid P-256 point. Decode
|
|
148
|
+
# from wire to exercise lazy public-key parsing without an OpenSSL cache.
|
|
149
|
+
malformed.from_data([malformed.flags, malformed.protocol, malformed.algorithm,
|
|
150
|
+
"\0" * 62 + [@a.key_tag - 1038].pack('n')])
|
|
151
|
+
message = Dnsruby::Message.new
|
|
152
|
+
message.add_answer(malformed)
|
|
153
|
+
malformed = Dnsruby::Message.decode(message.encode).answer.first
|
|
154
|
+
assert_equal(@a.key_tag, malformed.key_tag)
|
|
155
|
+
refute_equal(@a.key, malformed.key)
|
|
156
|
+
records = signed(@record, @a, 0)
|
|
157
|
+
[[malformed, @a], [@a, malformed]].each do |keys|
|
|
158
|
+
assert(@verifier.verify_rrset(records, keys))
|
|
159
|
+
end
|
|
160
|
+
error = assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, malformed) }
|
|
161
|
+
assert_match(/cryptographically verify/, error.message)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_invalid_signature_candidates_do_not_hide_a_valid_signature
|
|
165
|
+
[:expired, :future, :corrupted].each do |mode|
|
|
166
|
+
valid = signed(@record, @a, 0).sigs.first
|
|
167
|
+
invalid = invalid_signature(valid, mode)
|
|
168
|
+
[[invalid, valid], [valid, invalid]].each do |signatures|
|
|
169
|
+
records = Dnsruby::RRSet.new([@record] + signatures)
|
|
170
|
+
assert(@verifier.verify_rrset(records, @a))
|
|
171
|
+
end
|
|
172
|
+
records = Dnsruby::RRSet.new([@record, invalid])
|
|
173
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, @a) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_truncated_wire_rsa_and_dsa_keys_do_not_hide_another_valid_candidate
|
|
178
|
+
valid = signed(@record, @a, 0).sigs.first
|
|
179
|
+
[5, 8, 3].each do |algorithm|
|
|
180
|
+
['', "\0", "\xff", "\0\xff", "\xff\0\0", "\0\xff\xff\0"].each do |bytes|
|
|
181
|
+
malformed = wire_key(algorithm, bytes.b)
|
|
182
|
+
assert_raises(OpenSSL::PKey::PKeyError) { malformed.public_key }
|
|
183
|
+
invalid = valid.clone
|
|
184
|
+
invalid.algorithm = algorithm
|
|
185
|
+
invalid.key_tag = malformed.key_tag
|
|
186
|
+
invalid.signature = algorithm == 3 ? [bytes.getbyte(0) || 0].pack('C') + "\0" * 40 : "\0" * 64
|
|
187
|
+
records = Dnsruby::RRSet.new([@record, invalid, valid])
|
|
188
|
+
assert(@verifier.verify_rrset(records, [malformed, @a]))
|
|
189
|
+
assert(@verifier.verify_rrset(records, [@a, malformed]))
|
|
190
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, malformed) }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_extended_rsa_exponent_length_preserves_public_key_numbers
|
|
196
|
+
fixture = File.join(__dir__, 'fixtures', 'root-dnskey-2026-11-05.zone')
|
|
197
|
+
original = Dnsruby::RR.create(File.readlines(fixture).find { |line| line.include?(' IN DNSKEY ') })
|
|
198
|
+
length = original.key.getbyte(0)
|
|
199
|
+
extended = wire_key(8, "\0" + [length].pack('n') + original.key.byteslice(1..-1))
|
|
200
|
+
assert_raises(OpenSSL::PKey::PKeyError) { extended.public_key }
|
|
201
|
+
|
|
202
|
+
# Exercise both octets of the extended length with a 257-byte exponent.
|
|
203
|
+
exponent = "\x01" + "\0" * 255 + "\x03"
|
|
204
|
+
modulus = "\x7f" + "\xff" * 383
|
|
205
|
+
extended = wire_key(8, "\0" + [exponent.bytesize].pack('n') + exponent + modulus)
|
|
206
|
+
assert_equal(OpenSSL::BN.new(exponent, 2), extended.public_key.e)
|
|
207
|
+
assert_equal(OpenSSL::BN.new(modulus, 2), extended.public_key.n)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def test_dsa_is_rejected_and_does_not_hide_a_supported_signature
|
|
211
|
+
private_key = OpenSSL::PKey::DSA.new(DSA_PRIVATE_KEY)
|
|
212
|
+
bytes = [8].pack('C') + private_key.q.to_s(2).rjust(20, "\0") +
|
|
213
|
+
[:p, :g, :pub_key].map { |part| private_key.public_send(part).to_s(2).rjust(128, "\0") }.join
|
|
214
|
+
key = wire_key(3, bytes)
|
|
215
|
+
valid_ec = signed(@record, @a, 0).sigs.first
|
|
216
|
+
valid_dsa = valid_ec.clone
|
|
217
|
+
valid_dsa.algorithm = 3
|
|
218
|
+
valid_dsa.key_tag = key.key_tag
|
|
219
|
+
data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(@record, true) }.to_s
|
|
220
|
+
der = private_key.sign(OpenSSL::Digest::SHA1.new, valid_dsa.sig_data + data)
|
|
221
|
+
valid_dsa.signature = [8].pack('C') +
|
|
222
|
+
OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(20, "\0") }.join
|
|
223
|
+
error = assert_raises(Dnsruby::VerifyError) do
|
|
224
|
+
@verifier.verify_rrset(Dnsruby::RRSet.new([@record, valid_dsa]), key)
|
|
225
|
+
end
|
|
226
|
+
assert_match(/prohibited/, error.message)
|
|
227
|
+
|
|
228
|
+
['', "\0", [7].pack('C') + valid_dsa.signature.byteslice(1..-1), [8].pack('C') + "\0" * 40].each do |signature|
|
|
229
|
+
invalid = valid_dsa.clone
|
|
230
|
+
invalid.signature = signature
|
|
231
|
+
records = Dnsruby::RRSet.new([@record, invalid, valid_ec])
|
|
232
|
+
assert(@verifier.verify_rrset(records, [key, @a]))
|
|
233
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, key) }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_unsupported_signature_candidate_does_not_hide_supported_signature
|
|
238
|
+
unsupported = Dnsruby::RR.create("collision.invalid. 300 IN DNSKEY 257 3 17 #{Base64.strict_encode64(@a.key)}")
|
|
239
|
+
valid = signed(@record, @a, 0).sigs.first
|
|
240
|
+
invalid = valid.clone
|
|
241
|
+
invalid.algorithm = 17
|
|
242
|
+
invalid.key_tag = unsupported.key_tag
|
|
243
|
+
records = Dnsruby::RRSet.new([@record, invalid, valid])
|
|
244
|
+
assert(@verifier.verify_rrset(records, [unsupported, @a]))
|
|
245
|
+
assert(@verifier.verify_rrset(records, [@a, unsupported]))
|
|
246
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, unsupported) }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_invalid_signature_metadata_does_not_control_ds_discovery_expiration
|
|
250
|
+
[:expired, :future].each do |mode|
|
|
251
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
252
|
+
verifier.add_trust_anchor(Dnsruby::RR::DS.from_key(@a, 2))
|
|
253
|
+
valid = signed(@a, @a, 0).sigs.first
|
|
254
|
+
invalid = invalid_signature(valid, mode)
|
|
255
|
+
records = Dnsruby::RRSet.new([@a, invalid])
|
|
256
|
+
assert_raises(Dnsruby::VerifyError) { verifier.verify(records) }
|
|
257
|
+
assert_empty(verifier.get_keys_to_check)
|
|
258
|
+
records = Dnsruby::RRSet.new([@a, invalid, valid])
|
|
259
|
+
assert(verifier.verify(records))
|
|
260
|
+
assert_equal([Dnsruby::KeyCache.key_identity(@a)],
|
|
261
|
+
verifier.get_keys_to_check.map { |key| Dnsruby::KeyCache.key_identity(key) })
|
|
262
|
+
Time.stub(:now, @now + 3601) { assert_empty(verifier.get_keys_to_check) }
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def test_key_class_must_match_signature_class
|
|
267
|
+
foreign_key = Dnsruby::RR.create(@a.to_s)
|
|
268
|
+
foreign_key.klass = Dnsruby::Classes.CH
|
|
269
|
+
records = signed(@record, @a, 0)
|
|
270
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify_rrset(records, foreign_key) }
|
|
271
|
+
assert(@verifier.verify_rrset(records, [foreign_key, @a]))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_removing_one_collision_preserves_other_anchor_and_revokes_learned_trust
|
|
275
|
+
records_a, records_b = signed(@a, @a, 0), signed(@b, @b, 1)
|
|
276
|
+
[@a, @b].each { |key| @verifier.add_trust_anchor(key) }
|
|
277
|
+
assert(@verifier.verify(records_a))
|
|
278
|
+
assert(@verifier.verify(records_b))
|
|
279
|
+
assert_equal(@a, @verifier.remove_trust_anchor(@a))
|
|
280
|
+
assert_equal([@b], @verifier.trust_anchors)
|
|
281
|
+
assert_empty(@verifier.trusted_keys)
|
|
282
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(records_a) }
|
|
283
|
+
assert(@verifier.verify(records_b))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def test_removing_an_expired_pruned_anchor_revokes_its_learned_keys
|
|
287
|
+
@verifier.add_trust_anchor_with_expiration(@a, @now.to_i + 1)
|
|
288
|
+
@verifier.add_trust_anchor_with_expiration(@b, @now.to_i + 120)
|
|
289
|
+
records = signed(@a, @a, 0)
|
|
290
|
+
Time.stub(:now, @now) { assert(@verifier.verify(records)) }
|
|
291
|
+
Time.stub(:now, @now + 2) do
|
|
292
|
+
assert_equal([@b], @verifier.trust_anchors) # Naturally expire and prune A.
|
|
293
|
+
assert_empty(@verifier.trusted_keys)
|
|
294
|
+
assert_nil(@verifier.remove_trust_anchor(@a))
|
|
295
|
+
assert_equal([@b], @verifier.trust_anchors)
|
|
296
|
+
assert_empty(@verifier.trusted_keys)
|
|
297
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(records) }
|
|
298
|
+
end
|
|
299
|
+
Time.stub(:now, @now + 121) do
|
|
300
|
+
assert_empty(@verifier.trust_anchors)
|
|
301
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(records) }
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_expired_anchor_removal_prevents_in_flight_key_republication
|
|
306
|
+
@verifier.add_trust_anchor_with_expiration(@a, @now.to_i + 1)
|
|
307
|
+
@verifier.add_trust_anchor_with_expiration(@b, @now.to_i + 120)
|
|
308
|
+
records = signed(@a, @a, 0)
|
|
309
|
+
clock = @now
|
|
310
|
+
Time.stub(:now, -> { clock }) do
|
|
311
|
+
mutation = lambda do
|
|
312
|
+
clock = @now + 2
|
|
313
|
+
assert_equal([@b], @verifier.trust_anchors)
|
|
314
|
+
assert_nil(@verifier.remove_trust_anchor(@a))
|
|
315
|
+
end
|
|
316
|
+
pause_after_verification(mutation) { assert(@verifier.verify(records)) }
|
|
317
|
+
assert_equal([@b], @verifier.trust_anchors)
|
|
318
|
+
assert_empty(@verifier.trusted_keys)
|
|
319
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(records) }
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
def wire_key(algorithm, bytes)
|
|
326
|
+
key = Dnsruby::RR.create(@a.to_s)
|
|
327
|
+
key.algorithm = algorithm
|
|
328
|
+
key.instance_variable_set(:@key, bytes.b)
|
|
329
|
+
message = Dnsruby::Message.new
|
|
330
|
+
message.add_answer(key)
|
|
331
|
+
Dnsruby::Message.decode(message.encode).answer.first
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def invalid_signature(valid, mode)
|
|
335
|
+
invalid = valid.clone
|
|
336
|
+
case mode
|
|
337
|
+
when :expired
|
|
338
|
+
invalid.expiration = @now.to_i - 1
|
|
339
|
+
when :future
|
|
340
|
+
invalid.inception = @now.to_i + 3600
|
|
341
|
+
invalid.expiration = @now.to_i + 7200
|
|
342
|
+
when :corrupted
|
|
343
|
+
invalid.signature = "\0" * 64
|
|
344
|
+
end
|
|
345
|
+
invalid
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def signed(record, key, signer)
|
|
349
|
+
signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: 300,
|
|
350
|
+
type_covered: record.type, algorithm: 13, labels: record.name.labels.length,
|
|
351
|
+
original_ttl: 300, expiration: @now.to_i + 3600, inception: @now.to_i - 60,
|
|
352
|
+
key_tag: key.key_tag, signers_name: key.name)
|
|
353
|
+
data = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
|
|
354
|
+
der = @private_keys[signer].dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + data))
|
|
355
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
356
|
+
Dnsruby::RRSet.new([record, signature])
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def pause_after_verification(mutation)
|
|
360
|
+
arrived, resume, start = Queue.new, Queue.new, Queue.new
|
|
361
|
+
original = @verifier.method(:verify_rrset)
|
|
362
|
+
thread = nil
|
|
363
|
+
@verifier.define_singleton_method(:verify_rrset) do |*args|
|
|
364
|
+
result = original.call(*args)
|
|
365
|
+
if Thread.current.equal?(thread)
|
|
366
|
+
arrived << true
|
|
367
|
+
resume.pop
|
|
368
|
+
end
|
|
369
|
+
result
|
|
370
|
+
end
|
|
371
|
+
thread = Thread.new { start.pop; yield }
|
|
372
|
+
start << true
|
|
373
|
+
Timeout.timeout(3) { arrived.pop }
|
|
374
|
+
Timeout.timeout(3) { mutation.call }
|
|
375
|
+
resume << true
|
|
376
|
+
Timeout.timeout(3) { thread.value }
|
|
377
|
+
ensure
|
|
378
|
+
@verifier.singleton_class.remove_method(:verify_rrset) if original
|
|
379
|
+
thread.kill if thread && thread.alive?
|
|
380
|
+
thread.join if thread
|
|
381
|
+
end
|
|
382
|
+
end
|