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
|
@@ -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
|