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.
- 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 +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -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 +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- 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 +21 -2
- 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/OPT.rb +2 -3
- 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 +986 -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 +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -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 +127 -14
- data/.coveralls.yml +0 -2
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
# ++
|
|
16
16
|
|
|
17
|
+
require 'weakref'
|
|
18
|
+
require 'securerandom'
|
|
19
|
+
require 'dnsruby/serial_time'
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
# This class does verification/validation from a single point - signed root,
|
|
19
23
|
# DLV, trust anchors. Dnssec controls a set of these to perform validation for
|
|
@@ -21,52 +25,141 @@
|
|
|
21
25
|
# This class should only be used by Dnsruby
|
|
22
26
|
module Dnsruby
|
|
23
27
|
class SingleVerifier # :nodoc: all
|
|
28
|
+
# Configured anchors are separate; only learned trust is capacity-limited.
|
|
29
|
+
MAX_LEARNED_KEYS = 4096
|
|
30
|
+
MAX_LEARNED_DS_RECORDS = 4096
|
|
31
|
+
|
|
32
|
+
# RRsets carry only serializable provenance, never a verifier, mutex or
|
|
33
|
+
# validation budget. The weak registry lets another verifier check whether
|
|
34
|
+
# the original trust source has since been removed.
|
|
35
|
+
LearnedTrust = Struct.new(:expiration, :signature_expiration, :transaction, :sources, :operational)
|
|
36
|
+
VALIDATION_ALGORITHMS = [Algorithms.RSASHA1, Algorithms.RSASHA1_NSEC3_SHA1,
|
|
37
|
+
Algorithms.RSASHA256, Algorithms.RSASHA512,
|
|
38
|
+
Algorithms.ECDSAP256SHA256, Algorithms.ECDSAP384SHA384, Algorithms.ED25519, Algorithms.ED448].freeze
|
|
39
|
+
# RFC 9905 retains implementation support for RSA/SHA-1 but requires
|
|
40
|
+
# validating resolvers to treat it as unsupported operationally. Explicit
|
|
41
|
+
# verify() calls keep their existing cryptographic verification behaviour.
|
|
42
|
+
OPERATIONAL_ALGORITHMS = (VALIDATION_ALGORITHMS -
|
|
43
|
+
[Algorithms.RSASHA1, Algorithms.RSASHA1_NSEC3_SHA1]).freeze
|
|
44
|
+
private_constant :LearnedTrust, :VALIDATION_ALGORITHMS, :OPERATIONAL_ALGORITHMS
|
|
45
|
+
@provenance_mutex = Mutex.new
|
|
46
|
+
@trust_sources = {}
|
|
47
|
+
@next_source_id = 0
|
|
48
|
+
@source_namespace = SecureRandom.hex(16).freeze
|
|
49
|
+
|
|
50
|
+
def self.register_trust_source(verifier)
|
|
51
|
+
@provenance_mutex.synchronize do
|
|
52
|
+
@trust_sources.delete_if { |_id, source| !source.weakref_alive? }
|
|
53
|
+
@next_source_id += 1
|
|
54
|
+
identity = [@source_namespace, Process.pid, @next_source_id].freeze
|
|
55
|
+
@trust_sources[identity] = WeakRef.new(verifier)
|
|
56
|
+
identity
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.trust_source(id)
|
|
61
|
+
@provenance_mutex.synchronize do
|
|
62
|
+
source = @trust_sources[id]
|
|
63
|
+
source.__getobj__ if source && source.weakref_alive?
|
|
64
|
+
end
|
|
65
|
+
rescue WeakRef::RefError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
private_class_method :register_trust_source, :trust_source
|
|
69
|
+
|
|
24
70
|
class VerifierType
|
|
25
71
|
ROOT = 0
|
|
26
72
|
ANCHOR = 1
|
|
27
73
|
DLV = 2
|
|
28
74
|
end
|
|
29
75
|
def initialize(vtype)
|
|
76
|
+
@trust_mutex = Mutex.new
|
|
77
|
+
@trust_generation = 0
|
|
78
|
+
@trust_configuration_generation = 0
|
|
79
|
+
@trust_source_id = SingleVerifier.send(:register_trust_source, self)
|
|
30
80
|
@verifier_type = vtype
|
|
31
81
|
@added_dlv_key = false
|
|
32
|
-
# The DNSKEY RRs for the signed root (when it exists)
|
|
33
|
-
@root_anchors = KeyCache.new
|
|
34
|
-
|
|
35
82
|
# The set of trust anchors.
|
|
36
83
|
# If the root is unsigned, then these must be initialised with at least
|
|
37
84
|
# one trusted key by the client application, if verification is to be performed.
|
|
38
85
|
@trust_anchors = KeyCache.new
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
# Track configured expiration independently of KeyCache pruning so an
|
|
87
|
+
# expired anchor invalidates learned trust and in-flight publication.
|
|
88
|
+
@configured_dnskey_identities = {}
|
|
41
89
|
|
|
42
90
|
# The set of keys which are trusted.
|
|
43
|
-
@trusted_keys = KeyCache.new
|
|
91
|
+
@trusted_keys = KeyCache.new(max_keys: MAX_LEARNED_KEYS)
|
|
92
|
+
@trusted_key_provenance = {}
|
|
44
93
|
|
|
45
94
|
# The set of keys which have been indicated by a DS RRSet which has been
|
|
46
95
|
# signed by a trusted key. Although we have not yet located these keys, we
|
|
47
96
|
# have the details (tag and digest) which can identify the keys when we
|
|
48
97
|
# see them. At that point, they will be added to our trusted keys.
|
|
49
|
-
@discovered_ds_store =
|
|
98
|
+
@discovered_ds_store = DSCache.new(max_records: MAX_LEARNED_DS_RECORDS)
|
|
50
99
|
# The configured_ds_store is the set of DS records which have been configured
|
|
51
100
|
# by the client as trust anchors. Use Dnssec#add_trust_anchor to add these
|
|
52
101
|
@configured_ds_store = []
|
|
102
|
+
@configured_ds_expirations = {}
|
|
103
|
+
@configured_ds_index = DSCache.new(max_records: nil)
|
|
53
104
|
end
|
|
54
105
|
|
|
55
|
-
|
|
56
|
-
|
|
106
|
+
@recursor_mutex = Mutex.new
|
|
107
|
+
@recursor_pid = Process.pid
|
|
108
|
+
|
|
109
|
+
def self.with_recursor_lock
|
|
110
|
+
if @recursor_pid != Process.pid
|
|
111
|
+
@recursor_mutex = Mutex.new
|
|
112
|
+
@recursor_pid = Process.pid
|
|
113
|
+
@recursor = nil
|
|
114
|
+
end
|
|
115
|
+
locked = false
|
|
116
|
+
until (locked = @recursor_mutex.try_lock)
|
|
117
|
+
QueryControl.current&.check!
|
|
118
|
+
sleep 0.01
|
|
119
|
+
end
|
|
120
|
+
QueryControl.current&.check!
|
|
121
|
+
yield
|
|
122
|
+
ensure
|
|
123
|
+
@recursor_mutex.unlock if locked
|
|
57
124
|
end
|
|
125
|
+
private_class_method :with_recursor_lock
|
|
58
126
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
@@recursor = Recursor.new()
|
|
127
|
+
def self.set_recursor_hints(hints)
|
|
128
|
+
with_recursor_lock do
|
|
129
|
+
@recursor_hints = if hints.is_a?(Array)
|
|
130
|
+
hints.map { |hint| hint.dup.freeze }.freeze
|
|
64
131
|
else
|
|
65
|
-
|
|
132
|
+
hints&.dup&.freeze
|
|
66
133
|
end
|
|
134
|
+
@recursor = nil
|
|
135
|
+
Recursor.reset_hints
|
|
67
136
|
end
|
|
68
|
-
|
|
69
|
-
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def set_hints(hints)
|
|
140
|
+
SingleVerifier.set_recursor_hints(hints)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.shared_recursor
|
|
144
|
+
with_recursor_lock do
|
|
145
|
+
unless @recursor
|
|
146
|
+
if @recursor_hints
|
|
147
|
+
bootstrap = Resolver.new
|
|
148
|
+
begin
|
|
149
|
+
Recursor.set_hints(@recursor_hints, bootstrap)
|
|
150
|
+
ensure
|
|
151
|
+
bootstrap.close
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
@recursor = Recursor.new
|
|
155
|
+
@recursor.dnssec = true
|
|
156
|
+
end
|
|
157
|
+
@recursor
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def get_recursor
|
|
162
|
+
SingleVerifier.shared_recursor
|
|
70
163
|
end
|
|
71
164
|
|
|
72
165
|
def get_dlv_resolver # :nodoc:
|
|
@@ -91,6 +184,10 @@ module Dnsruby
|
|
|
91
184
|
end
|
|
92
185
|
end
|
|
93
186
|
def get_dlv_key(ksk) # :nodoc:
|
|
187
|
+
with_validation { get_dlv_key_internal(ksk) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private def get_dlv_key_internal(ksk)
|
|
94
191
|
# Using the KSK, get the ZSK for the DLV registry
|
|
95
192
|
if (!@res && (@verifier_type == VerifierType::DLV))
|
|
96
193
|
@res = get_dlv_resolver
|
|
@@ -102,6 +199,8 @@ module Dnsruby
|
|
|
102
199
|
if (!ret)
|
|
103
200
|
raise ResolvError.new("Couldn't get response from Recursor")
|
|
104
201
|
end
|
|
202
|
+
rescue RecursionLimitError
|
|
203
|
+
raise
|
|
105
204
|
rescue ResolvError => e
|
|
106
205
|
# print "ERROR - Couldn't find the DLV key\n"
|
|
107
206
|
TheLog.error("Couldn't find the DLV key\n")
|
|
@@ -124,28 +223,104 @@ module Dnsruby
|
|
|
124
223
|
end
|
|
125
224
|
# Add the
|
|
126
225
|
def add_trust_anchor_with_expiration(k, expiration)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
226
|
+
@trust_mutex.synchronize do
|
|
227
|
+
@trust_configuration_generation += 1
|
|
228
|
+
if (k.type == Types.DNSKEY)
|
|
229
|
+
@trust_anchors.add_key_with_expiration(k, expiration)
|
|
230
|
+
identity = KeyCache.key_identity(k)
|
|
231
|
+
@configured_dnskey_identities[identity] = [@configured_dnskey_identities[identity] || expiration, expiration].max
|
|
232
|
+
TheLog.info("Adding trust anchor for #{k.name}")
|
|
233
|
+
elsif ((k.type == Types.DS) || ((k.type == Types.DLV) && (@verifier_type == VerifierType::DLV)))
|
|
234
|
+
identity = ds_identity(k)
|
|
235
|
+
previous = @configured_ds_expirations[identity]
|
|
236
|
+
@configured_ds_store.push(k) unless previous
|
|
237
|
+
@configured_ds_expirations[identity] = [previous || expiration, expiration].max
|
|
238
|
+
index_configured_ds
|
|
239
|
+
end
|
|
134
240
|
end
|
|
135
241
|
end
|
|
136
242
|
|
|
137
243
|
def remove_trust_anchor(t)
|
|
138
|
-
@
|
|
244
|
+
@trust_mutex.synchronize do
|
|
245
|
+
configured = false
|
|
246
|
+
removed = if (t.type == Types.DNSKEY)
|
|
247
|
+
configured = @configured_dnskey_identities.delete(KeyCache.key_identity(t))
|
|
248
|
+
@trust_anchors.delete(t)
|
|
249
|
+
elsif t.type == Types.DS || t.type == Types.DLV
|
|
250
|
+
identity = ds_identity(t)
|
|
251
|
+
configured = @configured_ds_expirations.delete(identity)
|
|
252
|
+
found = @configured_ds_store.find { |record| ds_identity(record) == identity }
|
|
253
|
+
@configured_ds_store.delete(found) if found
|
|
254
|
+
index_configured_ds
|
|
255
|
+
found
|
|
256
|
+
end
|
|
257
|
+
# Cached keys and in-flight discoveries may depend on this anchor.
|
|
258
|
+
clear_learned_keys if removed || configured
|
|
259
|
+
removed
|
|
260
|
+
end
|
|
139
261
|
end
|
|
140
|
-
#
|
|
262
|
+
# Remove configured anchors and any keys learned from them.
|
|
141
263
|
def clear_trust_anchors
|
|
142
|
-
@
|
|
264
|
+
@trust_mutex.synchronize do
|
|
265
|
+
@trust_anchors = KeyCache.new
|
|
266
|
+
@configured_dnskey_identities = {}
|
|
267
|
+
@configured_ds_store = []
|
|
268
|
+
@configured_ds_expirations = {}
|
|
269
|
+
index_configured_ds
|
|
270
|
+
clear_learned_keys
|
|
271
|
+
end
|
|
143
272
|
end
|
|
144
273
|
|
|
145
274
|
def trust_anchors
|
|
146
|
-
|
|
275
|
+
@trust_mutex.synchronize do
|
|
276
|
+
expire_trust_anchors
|
|
277
|
+
@trust_anchors.keys + @configured_ds_store
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Response/failure caches need a stable, inexpensive identity for the
|
|
282
|
+
# current trust configuration. Additions invalidate response caches without
|
|
283
|
+
# invalidating otherwise valid in-flight learned-trust publication.
|
|
284
|
+
def validation_cache_identity # :nodoc:
|
|
285
|
+
@trust_mutex.synchronize do
|
|
286
|
+
expire_trust_anchors
|
|
287
|
+
[@trust_source_id, @trust_generation, @trust_configuration_generation].freeze
|
|
288
|
+
end
|
|
147
289
|
end
|
|
148
290
|
|
|
291
|
+
# Caller holds @trust_mutex. As with explicit removal, cached descendants
|
|
292
|
+
# cannot outlive a configured anchor. Other configured anchors remain
|
|
293
|
+
# available and can establish fresh trust without retaining stale descendants.
|
|
294
|
+
def expire_trust_anchors
|
|
295
|
+
now = Time.now.to_i
|
|
296
|
+
expired = @configured_dnskey_identities.any? { |_identity, expiration| expiration <= now } ||
|
|
297
|
+
@configured_ds_expirations.any? { |_identity, expiration| expiration <= now }
|
|
298
|
+
return unless expired
|
|
299
|
+
@configured_dnskey_identities.delete_if { |_identity, expiration| expiration <= now }
|
|
300
|
+
@trust_anchors.remove_expired_keys
|
|
301
|
+
@configured_ds_expirations.delete_if { |_identity, expiration| expiration <= now }
|
|
302
|
+
@configured_ds_store.select! { |record| @configured_ds_expirations.key?(ds_identity(record)) }
|
|
303
|
+
index_configured_ds
|
|
304
|
+
clear_learned_keys
|
|
305
|
+
end
|
|
306
|
+
def ds_identity(record)
|
|
307
|
+
[record.name.canonical.freeze, record.klass.code, record.type.code,
|
|
308
|
+
record.key_tag, record.algorithm.code, record.digest_type.code, record.digestbin&.dup&.freeze].freeze
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Caller holds @trust_mutex. Configured anchors are never capacity-evicted.
|
|
312
|
+
def index_configured_ds
|
|
313
|
+
@configured_ds_index = DSCache.new(max_records: nil)
|
|
314
|
+
@configured_ds_store.group_by { |record| DSCache.set_identity(record) }.each_value do |records|
|
|
315
|
+
rrset = RRSet.new
|
|
316
|
+
# Explicit anchors keep their configured objects and TTLs. Their
|
|
317
|
+
# deadlines are tracked separately from RRset TTL normalization.
|
|
318
|
+
records.each { |record| rrset.privateAdd(record) }
|
|
319
|
+
@configured_ds_index.add(rrset, Float::INFINITY)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
private :expire_trust_anchors, :ds_identity, :index_configured_ds
|
|
323
|
+
|
|
149
324
|
# Check that the RRSet and RRSIG record are compatible
|
|
150
325
|
def check_rr_data(rrset, sigrec)#:nodoc: all
|
|
151
326
|
# Each RR MUST have the same owner name as the RRSIG RR;
|
|
@@ -164,6 +339,22 @@ module Dnsruby
|
|
|
164
339
|
raise VerifyError.new("RRSET should have same type as RRSIG for verification")
|
|
165
340
|
end
|
|
166
341
|
|
|
342
|
+
# RFC 4035 5.3.1: a signature is authoritative only within its zone.
|
|
343
|
+
# DNSKEY is apex data; DS is parent-side data at a strict descendant.
|
|
344
|
+
zone = sigrec.signers_name
|
|
345
|
+
unless within_zone?(rrset.name, zone)
|
|
346
|
+
raise VerifyError, 'RRSet owner is outside the signing zone'
|
|
347
|
+
end
|
|
348
|
+
if rrset.type == Types.DNSKEY && rrset.name != zone
|
|
349
|
+
raise VerifyError, 'DNSKEY RRSet must be signed by its own zone'
|
|
350
|
+
end
|
|
351
|
+
if rrset.type == Types.DS && !rrset.name.subdomain_of?(zone)
|
|
352
|
+
raise VerifyError, 'DS RRSet must be signed by a parent zone'
|
|
353
|
+
end
|
|
354
|
+
if sigrec.labels < zone.labels.length
|
|
355
|
+
raise VerifyError, 'Wildcard source is outside the signing zone'
|
|
356
|
+
end
|
|
357
|
+
|
|
167
358
|
# #Each RR in the RRset MUST have the TTL listed in the
|
|
168
359
|
# #RRSIG Original TTL Field;
|
|
169
360
|
# if (rrset.ttl != sigrec.original_ttl)
|
|
@@ -172,65 +363,269 @@ module Dnsruby
|
|
|
172
363
|
|
|
173
364
|
# Now check that we are in the validity period for the RRSIG
|
|
174
365
|
now = Time.now.to_i
|
|
175
|
-
|
|
366
|
+
unless SerialTime.valid?(sigrec.inception, sigrec.expiration, now)
|
|
176
367
|
raise VerifyError.new("Signature record not in validity period")
|
|
177
368
|
end
|
|
178
369
|
end
|
|
179
370
|
|
|
371
|
+
# A bad or expired signature must not hide another usable signature.
|
|
372
|
+
# Filter before considering DS stores so invalid signature metadata cannot
|
|
373
|
+
# determine the expiration of a newly discovered key.
|
|
374
|
+
def valid_signatures(rrset)
|
|
375
|
+
error = nil
|
|
376
|
+
signatures = rrset.sigs.select do |sigrec|
|
|
377
|
+
ValidationBudget.current&.check!(1)
|
|
378
|
+
begin
|
|
379
|
+
check_rr_data(rrset, sigrec)
|
|
380
|
+
true
|
|
381
|
+
rescue VerifyError => e
|
|
382
|
+
error = e
|
|
383
|
+
false
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
raise error if signatures.empty? && error
|
|
387
|
+
signatures
|
|
388
|
+
end
|
|
389
|
+
private :valid_signatures
|
|
390
|
+
|
|
180
391
|
# Add the specified keys to the trusted key cache.
|
|
181
392
|
# k can be a KeyCache, or an RRSet of DNSKEYs.
|
|
182
|
-
def add_trusted_key(k)
|
|
183
|
-
|
|
393
|
+
def add_trusted_key(k, expiration = nil)
|
|
394
|
+
provenance = learned_provenance(k)
|
|
395
|
+
return false if provenance && !usable_provenance?(provenance)
|
|
396
|
+
expiration ||= provenance&.expiration
|
|
397
|
+
# Only an existing version of an incoming key can prevent its replacement.
|
|
398
|
+
# Readers check other provenance before use; checking the whole cache here
|
|
399
|
+
# makes each independent key discovery proportional to every cached zone.
|
|
400
|
+
incoming = k.is_a?(RRSet) ? k.rrs : (k.is_a?(KeyCache) ? k.keys : [])
|
|
401
|
+
identities = incoming.map { |key| KeyCache.key_identity(key) }
|
|
402
|
+
previous = @trust_mutex.synchronize do
|
|
403
|
+
identities.filter_map do |identity|
|
|
404
|
+
trust = @trusted_key_provenance[identity]
|
|
405
|
+
[identity, trust] if trust
|
|
406
|
+
end.to_h
|
|
407
|
+
end
|
|
408
|
+
invalid = previous.reject { |_identity, trust| usable_provenance?(trust) }
|
|
409
|
+
publish_trust do
|
|
410
|
+
invalid.each do |identity, trust|
|
|
411
|
+
next unless @trusted_key_provenance[identity].equal?(trust)
|
|
412
|
+
@trusted_keys.delete_identity(identity)
|
|
413
|
+
@trusted_key_provenance.delete(identity)
|
|
414
|
+
end
|
|
415
|
+
if provenance && (operation = trust_operation)
|
|
416
|
+
k.rrs.each { |key| operation[:keys][KeyCache.key_identity(key)] = [key, provenance] }
|
|
417
|
+
end
|
|
418
|
+
result = @trusted_keys.add(k, expiration)
|
|
419
|
+
if provenance && expiration && expiration > Time.now.to_i
|
|
420
|
+
k.rrs.each do |key|
|
|
421
|
+
identity = KeyCache.key_identity(key)
|
|
422
|
+
previous = @trusted_key_provenance[identity]
|
|
423
|
+
if !previous || (!previous.operational && provenance.operational) ||
|
|
424
|
+
(previous.operational == provenance.operational && previous.expiration < expiration)
|
|
425
|
+
@trusted_key_provenance[identity] = provenance
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
@trusted_key_provenance.delete_if { |identity, _trust| !@trusted_keys.include_identity?(identity) }
|
|
430
|
+
result
|
|
431
|
+
end
|
|
184
432
|
end
|
|
185
433
|
|
|
186
434
|
def add_root_ds(ds)
|
|
187
|
-
@
|
|
435
|
+
@trust_mutex.synchronize do
|
|
436
|
+
@trust_configuration_generation += 1
|
|
437
|
+
identity = ds_identity(ds)
|
|
438
|
+
@configured_ds_store.push(ds) unless @configured_ds_expirations.key?(identity)
|
|
439
|
+
@configured_ds_expirations[identity] = Float::INFINITY
|
|
440
|
+
index_configured_ds
|
|
441
|
+
end
|
|
188
442
|
end
|
|
189
443
|
|
|
190
|
-
#
|
|
444
|
+
# Clear learned trust and configured DS/DLV anchors, retaining DNSKEY anchors.
|
|
191
445
|
def clear_trusted_keys
|
|
192
|
-
@
|
|
446
|
+
@trust_mutex.synchronize do
|
|
447
|
+
clear_learned_keys
|
|
448
|
+
@configured_ds_store = []
|
|
449
|
+
@configured_ds_expirations = {}
|
|
450
|
+
index_configured_ds
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Caller holds @trust_mutex, also used by every learned-trust publication.
|
|
455
|
+
def clear_learned_keys
|
|
456
|
+
@trust_generation += 1
|
|
457
|
+
@trusted_keys = KeyCache.new(max_keys: MAX_LEARNED_KEYS)
|
|
458
|
+
@trusted_key_provenance = {}
|
|
193
459
|
@res = nil
|
|
194
|
-
@discovered_ds_store =
|
|
195
|
-
@configured_ds_store = []
|
|
460
|
+
@discovered_ds_store = DSCache.new(max_records: MAX_LEARNED_DS_RECORDS)
|
|
196
461
|
end
|
|
462
|
+
private :clear_learned_keys
|
|
197
463
|
|
|
198
464
|
def trusted_keys
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
465
|
+
keys, configured, discovered = @trust_mutex.synchronize do
|
|
466
|
+
expire_trust_anchors
|
|
467
|
+
[cached_trust_keys, @configured_ds_store.dup, @discovered_ds_store.rrsets]
|
|
468
|
+
end
|
|
469
|
+
keys.select { |key| usable_cached_key?(key) } + configured +
|
|
470
|
+
discovered.select { |rrset| usable_provenance?(learned_provenance(rrset)) }.flat_map(&:rrs)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Public verification and discovery methods enter here explicitly. Nested
|
|
474
|
+
# calls retain both the operation's limits and this verifier's trust generation.
|
|
475
|
+
def with_validation
|
|
476
|
+
ValidationBudget.with { with_trust_generation { yield } }
|
|
477
|
+
end
|
|
478
|
+
private :with_validation
|
|
479
|
+
|
|
480
|
+
# Nested verification and network discovery share the generation in which
|
|
481
|
+
# the outer operation started. No trust lock is held during either work.
|
|
482
|
+
def with_trust_generation
|
|
483
|
+
operations = Thread.current[:dnsruby_trust_generations] ||= {}
|
|
484
|
+
return yield if operations.key?(self)
|
|
485
|
+
generation = @trust_mutex.synchronize do
|
|
486
|
+
expire_trust_anchors
|
|
487
|
+
@trust_generation
|
|
488
|
+
end
|
|
489
|
+
operations[self] = { generation: generation, keys: {}, ds: [], rrsets: {}.compare_by_identity }
|
|
490
|
+
begin
|
|
491
|
+
yield
|
|
492
|
+
ensure
|
|
493
|
+
operations.delete(self)
|
|
494
|
+
Thread.current[:dnsruby_trust_generations] = nil if operations.empty?
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def publish_trust
|
|
499
|
+
generation = trust_operation&.fetch(:generation)
|
|
500
|
+
@trust_mutex.synchronize do
|
|
501
|
+
expire_trust_anchors
|
|
502
|
+
ValidationBudget.current&.check!
|
|
503
|
+
return false if generation && generation != @trust_generation
|
|
504
|
+
yield
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
private :with_trust_generation, :publish_trust
|
|
508
|
+
|
|
509
|
+
def trust_operation
|
|
510
|
+
Thread.current[:dnsruby_trust_generations]&.fetch(self, nil)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def learned_provenance(rrset)
|
|
514
|
+
return unless rrset.respond_to?(:type)
|
|
515
|
+
trust_operation&.fetch(:rrsets)&.fetch(rrset, nil) || rrset.instance_variable_get(:@dnsruby_learned_trust)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def current_trust_generation?(generation)
|
|
519
|
+
@trust_mutex.synchronize do
|
|
520
|
+
expire_trust_anchors
|
|
521
|
+
generation == @trust_generation
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def usable_provenance?(provenance)
|
|
526
|
+
return false unless provenance
|
|
527
|
+
return false if trust_operation&.fetch(:operational, false) && !provenance.operational
|
|
528
|
+
now = Time.now.to_i
|
|
529
|
+
return false if provenance.signature_expiration < now
|
|
530
|
+
return false unless provenance.expiration > now ||
|
|
531
|
+
provenance.transaction.equal?(ValidationBudget.current&.transaction)
|
|
532
|
+
provenance.sources.all? do |identity, generation|
|
|
533
|
+
source = SingleVerifier.send(:trust_source, identity)
|
|
534
|
+
source && source.send(:current_trust_generation?, generation)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def key_provenance(key)
|
|
539
|
+
identity = KeyCache.key_identity(key)
|
|
540
|
+
configured = @trust_mutex.synchronize do
|
|
541
|
+
expire_trust_anchors
|
|
542
|
+
@configured_dnskey_identities.key?(identity)
|
|
543
|
+
end
|
|
544
|
+
return if configured
|
|
545
|
+
local = trust_operation&.fetch(:keys)&.fetch(identity, nil)
|
|
546
|
+
return local.last if local
|
|
547
|
+
@trust_mutex.synchronize { @trusted_key_provenance[identity] }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def usable_cached_key?(key)
|
|
551
|
+
provenance = key_provenance(key)
|
|
552
|
+
!provenance || usable_provenance?(provenance)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Caller holds @trust_mutex. Keep provenance bounded by the live key cache.
|
|
556
|
+
def cached_trust_keys(signatures = nil)
|
|
557
|
+
keys = signatures ? @trusted_keys.matching_keys(signatures) : @trusted_keys.keys
|
|
558
|
+
unless signatures
|
|
559
|
+
@trusted_key_provenance.delete_if { |identity, _trust| !@trusted_keys.include_identity?(identity) }
|
|
560
|
+
end
|
|
561
|
+
keys
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def admit_provenance(provenance)
|
|
565
|
+
return unless provenance
|
|
566
|
+
raise VerifyError, 'Learned trust is no longer valid' unless usable_provenance?(provenance)
|
|
567
|
+
LearnedTrust.new(provenance.expiration, provenance.signature_expiration,
|
|
568
|
+
ValidationBudget.current.transaction, provenance.sources, provenance.operational).freeze
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def admit_learned_rrset(rrset)
|
|
572
|
+
provenance = learned_provenance(rrset)
|
|
573
|
+
return unless provenance
|
|
574
|
+
admitted = admit_provenance(provenance)
|
|
575
|
+
trust_operation[:rrsets][rrset] = admitted
|
|
576
|
+
admitted
|
|
206
577
|
end
|
|
207
578
|
|
|
579
|
+
def authenticated_provenance(rrset, signature, expiration, parent = nil)
|
|
580
|
+
# Retain the original receipt deadline when the same response is checked
|
|
581
|
+
# again. Cache lifetime and permission to finish this request are distinct.
|
|
582
|
+
previous = learned_provenance(rrset)
|
|
583
|
+
expiration = [expiration, previous.expiration].min if previous
|
|
584
|
+
signature_expiration = SerialTime.absolute(signature.expiration, Time.now.to_i)
|
|
585
|
+
raise VerifyError, 'Signature expiration is ambiguous' unless signature_expiration
|
|
586
|
+
if parent
|
|
587
|
+
expiration = [expiration, parent.expiration].min
|
|
588
|
+
signature_expiration = [signature_expiration, parent.signature_expiration].min
|
|
589
|
+
end
|
|
590
|
+
sources = (parent ? parent.sources : []) + [[@trust_source_id, trust_operation.fetch(:generation)].freeze]
|
|
591
|
+
LearnedTrust.new(expiration, signature_expiration, ValidationBudget.current.transaction,
|
|
592
|
+
sources.uniq.freeze, algorithm_supported?(signature.algorithm, operational: true) &&
|
|
593
|
+
(!parent || parent.operational)).freeze
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def retain_learned_rrset(rrset, provenance)
|
|
597
|
+
# The frozen value is safe to retain across verifier fallback, cloning,
|
|
598
|
+
# cache eviction, and Marshal round trips.
|
|
599
|
+
rrset.instance_variable_set(:@dnsruby_learned_trust, provenance)
|
|
600
|
+
rrset.rrs.each { |rr| rr.instance_variable_set(:@dnsruby_learned_trust, provenance) }
|
|
601
|
+
trust_operation[:rrsets][rrset] = provenance if trust_operation
|
|
602
|
+
end
|
|
603
|
+
private :trust_operation, :learned_provenance, :current_trust_generation?,
|
|
604
|
+
:usable_provenance?, :key_provenance, :usable_cached_key?,
|
|
605
|
+
:authenticated_provenance, :retain_learned_rrset, :cached_trust_keys,
|
|
606
|
+
:admit_provenance, :admit_learned_rrset
|
|
607
|
+
|
|
208
608
|
# Check that the key fits a signed DS record key details
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
found = true
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
}
|
|
609
|
+
# A DS digest match identifies a candidate; only a verified DNSKEY RRset
|
|
610
|
+
# can publish that key and establish its authenticated cache lifetime.
|
|
611
|
+
def check_ds(key, ds_rrset, expiration = nil)#:nodoc: all
|
|
612
|
+
with_validation { check_ds_internal(key, ds_rrset, expiration) }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
private def check_ds_internal(key, ds_rrset, expiration)
|
|
616
|
+
unless expiration
|
|
617
|
+
verification = {}
|
|
618
|
+
verify_rrset(ds_rrset, nil, verification)
|
|
619
|
+
expiration = verification.fetch(:expiration)
|
|
620
|
+
end
|
|
621
|
+
provenance = learned_provenance(ds_rrset)
|
|
622
|
+
return false if provenance && !usable_provenance?(provenance)
|
|
623
|
+
return false if !provenance && expiration < Time.now.to_i
|
|
624
|
+
preferred_ds_records(ds_rrset.rrs).any? do |ds|
|
|
625
|
+
ValidationBudget.current&.check!(1)
|
|
626
|
+
next false unless ds.check_key(key)
|
|
627
|
+
true
|
|
232
628
|
end
|
|
233
|
-
return found
|
|
234
629
|
end
|
|
235
630
|
|
|
236
631
|
# Verify the specified message (or RRSet) using the set of trusted keys.
|
|
@@ -247,6 +642,17 @@ module Dnsruby
|
|
|
247
642
|
#
|
|
248
643
|
# Returns true if the message verifies OK, and false otherwise.
|
|
249
644
|
def verify(msg, keys = nil)
|
|
645
|
+
verify_result(msg, keys) == :secure
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def verify_result(msg, keys = nil) # :nodoc:
|
|
649
|
+
with_validation do
|
|
650
|
+
result = verify_internal(msg, keys)
|
|
651
|
+
result == true ? :secure : result
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
private def verify_internal(msg, keys)
|
|
250
656
|
if (msg.kind_of?RRSet)
|
|
251
657
|
if (msg.type == Types.DNSKEY)
|
|
252
658
|
return verify_key_rrset(msg, keys)
|
|
@@ -262,40 +668,74 @@ module Dnsruby
|
|
|
262
668
|
# new total set of keys to check the rest of the rrsets.
|
|
263
669
|
# Return true if we can verify the whole message.
|
|
264
670
|
|
|
265
|
-
msg
|
|
671
|
+
discard_unsupported_nsec3_proofs(msg)
|
|
672
|
+
sections = msg.section_rrsets
|
|
673
|
+
context = { message: msg, proofs: {}, sections: sections,
|
|
674
|
+
verified: {}.compare_by_identity, discarded_optional: {}.compare_by_identity }
|
|
675
|
+
insecure = trust_operation&.fetch(:insecure_rrsets, {}) || {}
|
|
676
|
+
insecure_set = ->(set) { insecure[[set.name.canonical, set.klass.code, set.type.code]] }
|
|
677
|
+
sections.each do |section, rrsets|
|
|
266
678
|
# print "Checking section : #{section}\n"
|
|
267
|
-
ds_rrsets =
|
|
679
|
+
ds_rrsets = rrsets.select { |rrset| rrset.type == Types.DS }
|
|
268
680
|
if ((!ds_rrsets || ds_rrsets.length == 0) && (@verifier_type == VerifierType::DLV))
|
|
269
|
-
ds_rrsets =
|
|
681
|
+
ds_rrsets = rrsets.select { |rrset| rrset.type == Types.DLV }
|
|
270
682
|
end
|
|
271
683
|
ds_rrsets.each {|ds_rrset|
|
|
272
|
-
if
|
|
273
|
-
|
|
274
|
-
|
|
684
|
+
next if insecure_set.call(ds_rrset)
|
|
685
|
+
if section == 'additional'
|
|
686
|
+
verify_optional_rrset(ds_rrset, context) do |optional_context|
|
|
687
|
+
verify_ds_rrset(ds_rrset, keys, msg, optional_context)
|
|
688
|
+
end
|
|
689
|
+
elsif ds_rrset.rrs.any? && !verify_ds_rrset(ds_rrset, keys, msg, context)
|
|
690
|
+
raise VerifyError, 'Failed to verify DS RRSet'
|
|
275
691
|
end
|
|
276
692
|
}
|
|
277
693
|
|
|
278
|
-
key_rrsets =
|
|
694
|
+
key_rrsets = rrsets.select { |rrset| rrset.type == Types.DNSKEY }
|
|
279
695
|
key_rrsets.each {|key_rrset|
|
|
280
|
-
if
|
|
281
|
-
|
|
282
|
-
|
|
696
|
+
next if insecure_set.call(key_rrset)
|
|
697
|
+
if section == 'additional'
|
|
698
|
+
verify_optional_rrset(key_rrset, context) do |optional_context|
|
|
699
|
+
verify_key_rrset(key_rrset, keys, optional_context)
|
|
700
|
+
end
|
|
701
|
+
elsif key_rrset.rrs.any? && !verify_key_rrset(key_rrset, keys, context)
|
|
702
|
+
raise VerifyError, 'Failed to verify DNSKEY RRSet'
|
|
283
703
|
end
|
|
284
704
|
}
|
|
285
705
|
end
|
|
286
706
|
|
|
287
|
-
verify_nsecs(msg)
|
|
288
|
-
|
|
289
707
|
# Then, look through all the remaining RRSets, and verify them all (unless not necessary).
|
|
290
|
-
|
|
708
|
+
sections.each do |section, rrsets|
|
|
291
709
|
rrsets.each do |rrset|
|
|
710
|
+
next if context[:discarded_optional].key?(rrset)
|
|
711
|
+
if section != 'additional' && insecure_set.call(rrset)
|
|
712
|
+
context[:insecure] = true
|
|
713
|
+
next
|
|
714
|
+
end
|
|
715
|
+
# Key learning already authenticated these exact per-message sets.
|
|
716
|
+
next if context[:verified].key?(rrset)
|
|
717
|
+
if section == 'answer' && rrset.type == Types.CNAME && rrset.sigs.empty?
|
|
718
|
+
next if verify_synthesized_cname(rrset, keys, context)
|
|
719
|
+
end
|
|
292
720
|
# If delegation NS or glue AAAA/A, then don't expect RRSIG.
|
|
293
721
|
# Otherwise, expect RRSIG and fail verification if RRSIG is not present
|
|
294
722
|
|
|
295
723
|
if ((section == "authority") && (rrset.type == Types.NS))
|
|
724
|
+
# Parent-side delegation NS records are unsigned, including when
|
|
725
|
+
# the query is below the cut and its DS RRset authenticated above.
|
|
726
|
+
question = msg.question.first
|
|
727
|
+
terminal, answered = terminal_answer(msg, question) if question
|
|
728
|
+
if question && !answered && rrset.klass == question.qclass &&
|
|
729
|
+
within_zone?(terminal, rrset.name) && rrset.sigs.empty? &&
|
|
730
|
+
rrsets.any? { |set| set.type == Types.DS && set.name == rrset.name &&
|
|
731
|
+
set.klass == rrset.klass && context[:verified].key?(set) &&
|
|
732
|
+
rrset.name.subdomain_of?(context[:verified][set][:signature].signers_name) }
|
|
733
|
+
next
|
|
734
|
+
end
|
|
296
735
|
# Check for delegation
|
|
297
|
-
dsrrset =
|
|
298
|
-
if
|
|
736
|
+
dsrrset = rrsets.find { |set| set.type == Types.DS && set.name == rrset.name && set.klass == rrset.klass }
|
|
737
|
+
if question && !answered && rrset.klass == question.qclass &&
|
|
738
|
+
within_zone?(terminal, rrset.name) && !dsrrset
|
|
299
739
|
# Now check NSEC(3) records for absence of DS and SOA
|
|
300
740
|
nsec = msg.authority.rrsets('NSEC')[0]
|
|
301
741
|
if (!nsec || (nsec.length == 0))
|
|
@@ -326,286 +766,420 @@ module Dnsruby
|
|
|
326
766
|
end
|
|
327
767
|
end
|
|
328
768
|
|
|
329
|
-
if
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
769
|
+
if section == 'additional'
|
|
770
|
+
# Unsigned glue is needed for iterative discovery. Signed optional
|
|
771
|
+
# data is useful only when it verifies without another lookup; a
|
|
772
|
+
# failure must not reject an independently authenticated answer.
|
|
773
|
+
if rrset.sigs.any?
|
|
774
|
+
verify_optional_rrset(rrset, context) do |optional_context|
|
|
775
|
+
verify_rrset(rrset, keys, optional_context)
|
|
336
776
|
end
|
|
337
|
-
ns_rrsets = msg.additional.rrsets('NS')
|
|
338
|
-
ns_rrsets.each {|ns_rrset|
|
|
339
|
-
if (ns_rrset.length > 0)
|
|
340
|
-
nsname = ns_rrset.rrs()[0].name
|
|
341
|
-
if (arec && arec.rrs().length > 0)
|
|
342
|
-
aname = arec.rrs()[0].name
|
|
343
|
-
if (nsname.subdomain_of?aname)
|
|
344
|
-
next
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
}
|
|
349
777
|
end
|
|
350
|
-
end
|
|
351
|
-
# If records are in additional, and no RRSIG, that's Ok - just don't use them!
|
|
352
|
-
if ((section == "additional") && (rrset.sigs.length == 0))
|
|
353
|
-
# @TODO@ Make sure that we don't cache these records!
|
|
354
778
|
next
|
|
355
779
|
end
|
|
356
780
|
# else verify RRSet
|
|
357
781
|
# print "About to verify #{rrset.name}, #{rrset.type}\n"
|
|
358
|
-
if (!verify_rrset(rrset, keys))
|
|
782
|
+
if (!verify_rrset(rrset, keys, context))
|
|
359
783
|
# print "FAILED TO VERIFY RRSET #{rrset.name}, #{rrset.type}\n"
|
|
360
784
|
TheLog.debug("Failed to verify rrset")
|
|
361
785
|
return false
|
|
362
786
|
end
|
|
363
787
|
end
|
|
364
788
|
end
|
|
365
|
-
|
|
789
|
+
verify_nsecs(msg, context)
|
|
790
|
+
discard_optional_rrsets(msg, context)
|
|
791
|
+
apply_verified_lifetimes(msg, context)
|
|
792
|
+
return context[:insecure] ? :insecure : true
|
|
366
793
|
end
|
|
367
794
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return check_no_wildcard_expansion(msg)
|
|
405
|
-
elsif (msg.rcode == RCode.NOERROR)
|
|
406
|
-
if (msg.answer.length > 0)
|
|
407
|
-
# print "Checking NSECs for wildcard expansion\n"
|
|
408
|
-
# wildcard expansion answer - check NSECs!
|
|
409
|
-
# We want to make sure that the NSEC tells us that there is no closer match for this name
|
|
410
|
-
# @TODO@ We need to make replace the RRSIG name with the wildcard name before we can verify it correctly.
|
|
411
|
-
check_num_rrsig_labels(msg)
|
|
412
|
-
return check_name_in_nsecs(msg, msg.question()[0].qtype, true)
|
|
413
|
-
else
|
|
414
|
-
# Either no data or wildcard no data - check to see which
|
|
415
|
-
# Should be able to tell this by checking the number of labels in the NSEC records.
|
|
416
|
-
# Sort these two last cases out!
|
|
417
|
-
isWildcardNoData = false
|
|
418
|
-
[msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
|
|
419
|
-
nsec_rrsets.each {|nsec_rrset|
|
|
420
|
-
nsec_rrset.rrs.each {|nsec|
|
|
421
|
-
# print "Checking nsec to see if wildcard : #{nsec}\n"
|
|
422
|
-
if (nsec.name.wild? ||(nsec.name.labels.length < msg.question()[0].qname.labels.length))
|
|
423
|
-
isWildcardNoData = true
|
|
424
|
-
end
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
795
|
+
# Unsupported NSEC3 parameters do not invalidate another usable proof.
|
|
796
|
+
# Keep a mixed RRset intact for signature verification; usable_hash? filters
|
|
797
|
+
# its unsupported records when the proof is evaluated. Explicit NSEC3
|
|
798
|
+
# answers remain available to callers inspecting these records.
|
|
799
|
+
def discard_unsupported_nsec3_proofs(message)
|
|
800
|
+
supported = {}
|
|
801
|
+
message.authority.each do |record|
|
|
802
|
+
ValidationBudget.current&.check!(1)
|
|
803
|
+
next unless record.type == Types.NSEC3
|
|
804
|
+
identity = [record.name.canonical, record.klass.code]
|
|
805
|
+
supported[identity] = !!supported[identity] || record.supported_parameters?
|
|
806
|
+
end
|
|
807
|
+
return unless supported.value?(false)
|
|
808
|
+
message.authority.delete_if do |record|
|
|
809
|
+
ValidationBudget.current&.check!(1)
|
|
810
|
+
type = record.type == Types.RRSIG ? record.type_covered : record.type
|
|
811
|
+
type == Types.NSEC3 && supported[[record.name.canonical, record.klass.code]] == false
|
|
812
|
+
end
|
|
813
|
+
message.update_counts
|
|
814
|
+
end
|
|
815
|
+
private :discard_unsupported_nsec3_proofs
|
|
816
|
+
|
|
817
|
+
# Optional verification may contribute authenticated data, but never the
|
|
818
|
+
# security classification of the required answer. Drop failed signed sets
|
|
819
|
+
# from the returned message so response caching cannot retain them as part
|
|
820
|
+
# of a successfully validated response. Resource limits still abort all work.
|
|
821
|
+
def verify_optional_rrset(rrset, context)
|
|
822
|
+
optional_context = context.merge(insecure: false)
|
|
823
|
+
return true if yield(optional_context) && !optional_context[:insecure]
|
|
824
|
+
discard_optional_rrset(rrset, context)
|
|
825
|
+
false
|
|
826
|
+
rescue VerifyError => error
|
|
827
|
+
raise if error.is_a?(ValidationLimitError)
|
|
828
|
+
discard_optional_rrset(rrset, context)
|
|
829
|
+
false
|
|
830
|
+
end
|
|
428
831
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
832
|
+
def discard_optional_rrset(rrset, context)
|
|
833
|
+
context[:discarded_optional][rrset] = true
|
|
834
|
+
context[:verified].delete(rrset)
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def discard_optional_rrsets(message, context)
|
|
838
|
+
return if context[:discarded_optional].empty?
|
|
839
|
+
discarded = context[:discarded_optional].each_key.each_with_object({}) do |rrset, index|
|
|
840
|
+
ValidationBudget.current&.check!(1)
|
|
841
|
+
type = rrset.type == Types.RRSIG ? rrset.sigs.first.type_covered : rrset.type
|
|
842
|
+
index[[rrset.name.canonical, rrset.klass.code, type.code]] = true
|
|
843
|
+
end
|
|
844
|
+
message.additional.delete_if do |record|
|
|
845
|
+
ValidationBudget.current&.check!(1)
|
|
846
|
+
type = record.type == Types.RRSIG ? record.type_covered : record.type
|
|
847
|
+
discarded[[record.name.canonical, record.klass.code, type.code]]
|
|
848
|
+
end
|
|
849
|
+
message.update_counts
|
|
850
|
+
end
|
|
851
|
+
private :verify_optional_rrset, :discard_optional_rrset, :discard_optional_rrsets
|
|
852
|
+
|
|
853
|
+
# RFC 6672 5.3.1: a synthesized CNAME inherits authentication from the
|
|
854
|
+
# DNAME, provided that the complete label substitution is identical. Only
|
|
855
|
+
# answer-section DNAMEs can authenticate this unsigned answer CNAME.
|
|
856
|
+
def verify_synthesized_cname(cname, keys, context)
|
|
857
|
+
dname = context[:sections].fetch('answer').select do |set|
|
|
858
|
+
set.type == Types.DNAME && set.klass == cname.klass && cname.name.subdomain_of?(set.name)
|
|
859
|
+
end.max_by { |set| set.name.labels.length }
|
|
860
|
+
return false unless dname
|
|
861
|
+
unless cname.rrs.length == 1 && dname.rrs.length == 1 &&
|
|
862
|
+
cname.rrs.first.domainname == dname_substitution(cname.name, dname.rrs.first)
|
|
863
|
+
raise VerifyError, 'CNAME does not match its DNAME substitution'
|
|
864
|
+
end
|
|
865
|
+
unless context[:verified].key?(dname)
|
|
866
|
+
return false unless verify_rrset(dname, keys, context)
|
|
867
|
+
end
|
|
868
|
+
expiration = context[:verified].fetch(dname).fetch(:expiration)
|
|
869
|
+
received_at = cname.rrs.first.instance_variable_get(:@dnsruby_received_at) || Time.now.to_i
|
|
870
|
+
previous = cname.rrs.first.instance_variable_get(:@dnsruby_authenticated_expiration)
|
|
871
|
+
expiration = [expiration, received_at + cname.ttl, previous].compact.min
|
|
872
|
+
cname.ttl = [cname.ttl, dname.ttl, [expiration - Time.now.to_i, 0].max].min
|
|
873
|
+
context[:verified][cname] = { signature: nil, expiration: expiration }
|
|
874
|
+
true
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def dname_substitution(name, dname)
|
|
878
|
+
Name.new(name.labels.first(name.labels.length - dname.name.labels.length) +
|
|
879
|
+
dname.domainname.labels, true)
|
|
880
|
+
rescue ResolvError => e
|
|
881
|
+
raise VerifyError, "Invalid DNAME substitution: #{e.message}"
|
|
882
|
+
end
|
|
883
|
+
private :verify_synthesized_cname, :dname_substitution
|
|
884
|
+
|
|
885
|
+
# Check denial at the end of an alias chain, not at the original QNAME.
|
|
886
|
+
# A CNAME-only answer without a negative SOA can legitimately be partial.
|
|
887
|
+
# Positive wildcard proofs are checked when their signatures verify.
|
|
888
|
+
def verify_nsecs(msg, context = nil) # :nodoc:
|
|
889
|
+
question = msg.question.first
|
|
890
|
+
return unless question && [RCode.NOERROR, RCode.NXDOMAIN].include?(msg.rcode)
|
|
891
|
+
name, answered = terminal_answer(msg, question)
|
|
892
|
+
if trust_operation&.fetch(:insecure_names, {})&.key?([name.canonical, question.qclass.code])
|
|
893
|
+
context[:insecure] = true if context
|
|
894
|
+
return
|
|
895
|
+
end
|
|
896
|
+
return if msg.rcode == RCode.NOERROR && answered
|
|
897
|
+
raise VerifyError, 'NXDOMAIN response contains the requested answer' if answered
|
|
898
|
+
|
|
899
|
+
authority = context ? context[:sections].fetch('authority') : msg.authority.rrsets
|
|
900
|
+
klass, qtype = question.qclass, question.qtype
|
|
901
|
+
soa = authority.select do |set|
|
|
902
|
+
set.type == Types.SOA && set.klass == klass && within_zone?(name, set.name)
|
|
903
|
+
end.max_by { |set| set.name.labels.length }
|
|
904
|
+
referral = if msg.rcode == RCode.NOERROR && !soa
|
|
905
|
+
authority.select do |set|
|
|
906
|
+
set.type == Types.NS && set.klass == klass && within_zone?(name, set.name)
|
|
907
|
+
end.max_by { |set| set.name.labels.length }
|
|
908
|
+
end
|
|
909
|
+
if referral
|
|
910
|
+
# A signed delegation with DS needs no denial. An unsigned referral
|
|
911
|
+
# proves the absence of DS at the delegation, not absence of QTYPE.
|
|
912
|
+
return if authority.any? { |set| set.type == Types.DS && set.name == referral.name && set.klass == klass }
|
|
913
|
+
name, qtype = referral.name, Types.DS
|
|
914
|
+
elsif msg.rcode == RCode.NOERROR && name != question.qname && !soa
|
|
915
|
+
return
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
proofs = authority.select { |set| [Types.NSEC, Types.NSEC3].include?(set.type) && set.klass == klass }
|
|
919
|
+
if proofs.empty?
|
|
920
|
+
raise VerifyError, "Missing denial proof for #{name}"
|
|
921
|
+
end
|
|
922
|
+
zone = soa&.name
|
|
923
|
+
if context && soa
|
|
924
|
+
signature = context[:verified][soa]&.fetch(:signature)
|
|
925
|
+
raise VerifyError, 'Negative SOA is not signed by its zone' unless signature && signature.signers_name == zone
|
|
926
|
+
end
|
|
927
|
+
groups = {}
|
|
928
|
+
proofs.each do |set|
|
|
929
|
+
ValidationBudget.current&.check!(1)
|
|
930
|
+
signature = context && context[:verified][set]&.fetch(:signature)
|
|
931
|
+
next if context && !signature
|
|
932
|
+
signing_zone = signature&.signers_name || (set.type == Types.NSEC3 ? set.name.strip_label : zone)
|
|
933
|
+
next unless signing_zone && within_zone?(name, signing_zone)
|
|
934
|
+
next if zone && signing_zone != zone
|
|
935
|
+
# DS denial at a non-root delegation must come from the parent.
|
|
936
|
+
next if qtype == Types.DS && name == signing_zone && !name.labels.empty?
|
|
937
|
+
next if signature && signature.labels != set.name.labels.length &&
|
|
938
|
+
!(set.name.wild? && signature.labels == set.name.labels.length - 1)
|
|
939
|
+
(groups[[signing_zone, set.type]] ||= []).concat(set.rrs)
|
|
940
|
+
end
|
|
941
|
+
insecure = false
|
|
942
|
+
groups.each do |(signing_zone, type), records|
|
|
943
|
+
result = if type == Types.NSEC3
|
|
944
|
+
nsec3_denial(records, name, qtype, signing_zone, msg.rcode, !!referral)
|
|
442
945
|
else
|
|
443
|
-
|
|
946
|
+
nsec_denial(records, name, qtype, signing_zone, msg.rcode, !!referral)
|
|
947
|
+
end
|
|
948
|
+
if result == :secure
|
|
949
|
+
return true
|
|
950
|
+
elsif result == :insecure
|
|
951
|
+
insecure = true
|
|
952
|
+
end
|
|
953
|
+
end
|
|
954
|
+
if insecure
|
|
955
|
+
context[:insecure] = true if context
|
|
956
|
+
if context && (expirations = trust_operation&.fetch(:insecurity_expirations, nil))
|
|
957
|
+
expirations << context[:verified].values.map { |verified| verified[:expiration] }.min
|
|
444
958
|
end
|
|
959
|
+
return true
|
|
445
960
|
end
|
|
961
|
+
raise VerifyError, "No authenticated denial proof for #{name}, #{qtype}"
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def terminal_answer(msg, question)
|
|
965
|
+
name = question.qname
|
|
966
|
+
seen = {}
|
|
967
|
+
loop do
|
|
968
|
+
ValidationBudget.current&.check!(1)
|
|
969
|
+
raise VerifyError, 'CNAME loop in answer' if seen[name.canonical]
|
|
970
|
+
seen[name.canonical] = true
|
|
971
|
+
records = msg.answer.select { |rr| rr.name == name && rr.klass == question.qclass && rr.type != Types.RRSIG }
|
|
972
|
+
return [name, true] if records.any? { |rr| question.qtype == Types.ANY || rr.type == question.qtype }
|
|
973
|
+
aliases = records.select { |rr| rr.type == Types.CNAME }
|
|
974
|
+
if aliases.empty?
|
|
975
|
+
# A DNSSEC-aware server may omit the synthesized CNAME (RFC 6672
|
|
976
|
+
# 5.3.1); the authenticated DNAME still redirects the question.
|
|
977
|
+
dnames = msg.answer.select do |rr|
|
|
978
|
+
rr.type == Types.DNAME && rr.klass == question.qclass && name.subdomain_of?(rr.name)
|
|
979
|
+
end
|
|
980
|
+
return [name, false] if dnames.empty?
|
|
981
|
+
closest = dnames.max_by { |rr| rr.name.labels.length }
|
|
982
|
+
if dnames.any? { |rr| rr.name == closest.name && rr.domainname != closest.domainname }
|
|
983
|
+
raise VerifyError, 'Conflicting DNAME targets in answer'
|
|
984
|
+
end
|
|
985
|
+
name = dname_substitution(name, closest)
|
|
986
|
+
next
|
|
987
|
+
end
|
|
988
|
+
targets = aliases.map(&:domainname).uniq
|
|
989
|
+
raise VerifyError, 'Conflicting CNAME targets in answer' unless targets.length == 1
|
|
990
|
+
name = targets.first
|
|
991
|
+
end
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def within_zone?(name, zone)
|
|
995
|
+
name == zone || name.subdomain_of?(zone)
|
|
996
|
+
end
|
|
446
997
|
|
|
998
|
+
def denial_bitmap?(record, qtype, referral = false)
|
|
999
|
+
types = record.types
|
|
1000
|
+
# ANY has no data-type bit. NSEC3 can represent an empty nonterminal;
|
|
1001
|
+
# an exact NSEC owner necessarily has at least its own NSEC RRset.
|
|
1002
|
+
return record.type == Types.NSEC3 && types.empty? if qtype == Types.ANY
|
|
1003
|
+
# A validated NSEC and its covering RRSIG exist regardless of these
|
|
1004
|
+
# bitmap bits (RFC 4035 section 5.4).
|
|
1005
|
+
return false if record.type == Types.NSEC && [Types.NSEC, Types.RRSIG].include?(qtype)
|
|
1006
|
+
return false if types.include?(qtype) || types.include?(Types.CNAME)
|
|
1007
|
+
return types.include?(Types.NS) && !types.include?(Types.SOA) if referral
|
|
1008
|
+
# A parent delegation cannot deny ordinary data in the child zone.
|
|
1009
|
+
qtype == Types.DS || !types.include?(Types.NS) || types.include?(Types.SOA)
|
|
447
1010
|
end
|
|
448
1011
|
|
|
449
|
-
def
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1012
|
+
def nsec3_denial(records, name, qtype, zone, rcode, referral)
|
|
1013
|
+
records = records.select do |rr|
|
|
1014
|
+
ValidationBudget.current&.check!(1)
|
|
1015
|
+
rr.name.strip_label == zone && rr.usable_hash?
|
|
1016
|
+
end
|
|
1017
|
+
return unless records.any?
|
|
1018
|
+
# Do not combine independently salted/iterated chains into one proof.
|
|
1019
|
+
parameters = records.map { |rr| [rr.hash_alg, rr.iterations, rr.salt] }.uniq
|
|
1020
|
+
return unless parameters.length == 1
|
|
1021
|
+
hashes = {}
|
|
1022
|
+
hash_for = lambda do |candidate|
|
|
1023
|
+
hashes[candidate.canonical] ||= records.first.hash_name(candidate)
|
|
1024
|
+
end
|
|
1025
|
+
matching = lambda do |candidate|
|
|
1026
|
+
hashed = hash_for.call(candidate)
|
|
1027
|
+
find_denial_record(records) { |rr| rr.owner_hash == hashed }
|
|
1028
|
+
end
|
|
1029
|
+
covering = lambda do |candidate|
|
|
1030
|
+
hashed = hash_for.call(candidate)
|
|
1031
|
+
find_denial_record(records) { |rr| rr.covers_hash?(hashed) }
|
|
1032
|
+
end
|
|
1033
|
+
exact = matching.call(name)
|
|
1034
|
+
if rcode == RCode.NOERROR && exact
|
|
1035
|
+
return referral ? :insecure : :secure if denial_bitmap?(exact, qtype, referral)
|
|
1036
|
+
return
|
|
1037
|
+
end
|
|
1038
|
+
return if exact
|
|
1039
|
+
|
|
1040
|
+
child, closest, next_cover = name, name.strip_label, nil
|
|
1041
|
+
while within_zone?(closest, zone)
|
|
1042
|
+
ValidationBudget.current&.check!(1)
|
|
1043
|
+
if (record = matching.call(closest))
|
|
1044
|
+
# RFC 5155 8.3: a DNAME or parent-side delegation is not an
|
|
1045
|
+
# authoritative closest encloser for names below it.
|
|
1046
|
+
return if record.types.include?(Types.DNAME) ||
|
|
1047
|
+
(record.types.include?(Types.NS) && !record.types.include?(Types.SOA))
|
|
1048
|
+
next_cover = covering.call(child)
|
|
1049
|
+
break
|
|
1050
|
+
end
|
|
1051
|
+
break if closest == zone
|
|
1052
|
+
child, closest = closest, closest.strip_label
|
|
455
1053
|
end
|
|
456
|
-
|
|
457
|
-
if
|
|
458
|
-
|
|
1054
|
+
return unless next_cover
|
|
1055
|
+
if rcode == RCode.NOERROR && qtype == Types.DS
|
|
1056
|
+
return :insecure if next_cover.opt_out?
|
|
1057
|
+
return
|
|
1058
|
+
end
|
|
1059
|
+
wildcard = Name.new([Name::Label.new('*')] + closest.labels, true)
|
|
1060
|
+
if rcode == RCode.NXDOMAIN
|
|
1061
|
+
return unless covering.call(wildcard)
|
|
1062
|
+
else
|
|
1063
|
+
wildcard_record = matching.call(wildcard)
|
|
1064
|
+
return unless wildcard_record && denial_bitmap?(wildcard_record, qtype)
|
|
459
1065
|
end
|
|
1066
|
+
next_cover.opt_out? ? :insecure : :secure
|
|
460
1067
|
end
|
|
461
1068
|
|
|
462
|
-
def
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
nsec_rrsets.each {|nsecs|
|
|
467
|
-
nsecs.rrs.each {|nsec|
|
|
468
|
-
# print "Checking NSEC : #{nsec}\n"
|
|
469
|
-
next if (nsec.name.wild?)
|
|
470
|
-
if (check_record_proves_no_wildcard(msg, nsec))
|
|
471
|
-
proven_no_wildcards = true
|
|
472
|
-
end
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
if (!proven_no_wildcards)
|
|
477
|
-
# print "No proof that no RRSets could match through wildcard expansion\n"
|
|
478
|
-
raise VerifyError.new("No proof that no RRSets could match through wildcard expansion")
|
|
1069
|
+
def nsec_denial(records, name, qtype, zone, rcode, referral)
|
|
1070
|
+
records = records.select do |rr|
|
|
1071
|
+
ValidationBudget.current&.check!(1)
|
|
1072
|
+
within_zone?(rr.name, zone) && within_zone?(rr.next_domain, zone)
|
|
479
1073
|
end
|
|
1074
|
+
return unless records.any?
|
|
1075
|
+
return unless consistent_nsec_intervals?(records)
|
|
1076
|
+
exact = find_denial_record(records) { |rr| rr.name == name }
|
|
1077
|
+
if rcode == RCode.NOERROR && exact
|
|
1078
|
+
return referral ? :insecure : :secure if denial_bitmap?(exact, qtype, referral)
|
|
1079
|
+
return
|
|
1080
|
+
end
|
|
1081
|
+
return if exact || referral
|
|
1082
|
+
if rcode == RCode.NOERROR && find_denial_record(records) { |rr| rr.next_domain.subdomain_of?(name) && interval_covers?(rr.name, rr.next_domain, name) }
|
|
1083
|
+
# An NSEC ending at a descendant proves an empty nonterminal exists.
|
|
1084
|
+
return :secure
|
|
1085
|
+
end
|
|
1086
|
+
return unless find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, name, zone) }
|
|
1087
|
+
closest = name.strip_label
|
|
1088
|
+
while within_zone?(closest, zone)
|
|
1089
|
+
break if find_denial_record(records) { |rr| within_zone?(rr.name, closest) || within_zone?(rr.next_domain, closest) }
|
|
1090
|
+
return if closest == zone
|
|
1091
|
+
closest = closest.strip_label
|
|
1092
|
+
end
|
|
1093
|
+
next_closer = Name.new(name.labels.last(closest.labels.length + 1), true)
|
|
1094
|
+
return unless find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, next_closer, zone) }
|
|
1095
|
+
wildcard = Name.new([Name::Label.new('*')] + closest.labels, true)
|
|
1096
|
+
if rcode == RCode.NXDOMAIN
|
|
1097
|
+
return :secure if find_denial_record(records) { |rr| nsec_covers_next_closer?(rr, wildcard, zone) }
|
|
1098
|
+
else
|
|
1099
|
+
record = find_denial_record(records) { |rr| rr.name == wildcard }
|
|
1100
|
+
return :secure if record && denial_bitmap?(record, qtype)
|
|
1101
|
+
end
|
|
1102
|
+
nil
|
|
1103
|
+
end
|
|
480
1104
|
|
|
1105
|
+
def find_denial_record(records)
|
|
1106
|
+
records.find do |record|
|
|
1107
|
+
ValidationBudget.current&.check!(1)
|
|
1108
|
+
yield record
|
|
1109
|
+
end
|
|
481
1110
|
end
|
|
482
1111
|
|
|
483
|
-
def
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
1112
|
+
def consistent_nsec_intervals?(records)
|
|
1113
|
+
# If an interval contains any observed owner, it contains the next
|
|
1114
|
+
# distinct owner in canonical order. Sorting avoids an all-pairs scan.
|
|
1115
|
+
owners = records.group_by do |rr|
|
|
1116
|
+
ValidationBudget.current&.check!(1)
|
|
1117
|
+
rr.name.canonical
|
|
1118
|
+
end.values.sort do |first, second|
|
|
1119
|
+
ValidationBudget.current&.check!(1)
|
|
1120
|
+
first.first.name.canonically_before(second.first.name) ? -1 : 1
|
|
1121
|
+
end
|
|
1122
|
+
owners.each_with_index do |group, index|
|
|
1123
|
+
following = owners[(index + 1) % owners.length].first.name
|
|
1124
|
+
group.each do |rr|
|
|
1125
|
+
ValidationBudget.current&.check!(1)
|
|
1126
|
+
return false if interval_covers?(rr.name, rr.next_domain, following)
|
|
493
1127
|
end
|
|
494
1128
|
end
|
|
495
|
-
|
|
1129
|
+
true
|
|
496
1130
|
end
|
|
497
1131
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (qtype && !type_covered_checked)
|
|
533
|
-
# print "Tyes covered wrong for #{name}\n"
|
|
534
|
-
raise VerifyError.new("Types covered wrong for #{name}")
|
|
535
|
-
end
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
def check_name_in_and_type_not_in_nsecs(msg) # :nodoc:
|
|
539
|
-
check_name_in_nsecs(msg, msg.question()[0].qtype, false)
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
def check_name_not_in_wildcard_nsecs(msg) # :nodoc:
|
|
543
|
-
# @TODO@ Do this for NSEC3 records too!
|
|
544
|
-
name = msg.question()[0].qname
|
|
545
|
-
qtype = msg.question()[0].qtype
|
|
546
|
-
done= false
|
|
547
|
-
[msg.authority.rrsets('NSEC'), msg.authority.rrsets('NSEC3')].each {|nsec_rrsets|
|
|
548
|
-
nsec_rrsets.each {|nsecs|
|
|
549
|
-
nsecs.rrs.each {|nsec|
|
|
550
|
-
# print "Checking NSEC : #{nsec}\n"
|
|
551
|
-
next if !nsec.name.wild?
|
|
552
|
-
# Check the wildcard expansion
|
|
553
|
-
# We want to see that the name is in the wildcard range, and that the type
|
|
554
|
-
# is not in the types for the NSEC
|
|
555
|
-
if nsec.check_name_in_wildcard_range(name)
|
|
556
|
-
# print "Wildcard expansion in #{nsec} includes #{name}\n"
|
|
557
|
-
raise VerifyError.new("Wildcard expansion in #{nsec} includes #{name}")
|
|
558
|
-
# return false
|
|
559
|
-
end
|
|
560
|
-
if (nsec.types.include?qtype)
|
|
561
|
-
# print "#{qtype} present in wildcard #{nsec}\n"
|
|
562
|
-
raise VerifyError.new("#{qtype} present in wildcard #{nsec}")
|
|
563
|
-
# return false
|
|
564
|
-
end
|
|
565
|
-
done = true
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return if done
|
|
570
|
-
# print("Expected wildcard expansion in #{msg}\n")
|
|
571
|
-
raise VerifyError.new("Expected wildcard expansion in #{msg}")
|
|
572
|
-
# return false
|
|
1132
|
+
# Section#rrsets intentionally returns copies. Publish their authenticated
|
|
1133
|
+
# bounds only after the entire message has passed validation, including its
|
|
1134
|
+
# denial proof, so the response cache receives the same lifetimes.
|
|
1135
|
+
def apply_verified_lifetimes(msg, context)
|
|
1136
|
+
now = Time.now.to_i
|
|
1137
|
+
context[:sections].each do |section, rrsets|
|
|
1138
|
+
limits = {}
|
|
1139
|
+
rrsets.each do |rrset|
|
|
1140
|
+
ValidationBudget.current&.check!(1)
|
|
1141
|
+
result = context[:verified][rrset]
|
|
1142
|
+
next unless result
|
|
1143
|
+
limits[[rrset.name.canonical, rrset.klass.code, rrset.type.code]] =
|
|
1144
|
+
[rrset.ttl, result[:expiration]]
|
|
1145
|
+
end
|
|
1146
|
+
# An unsigned Additional set must not inherit authentication from a
|
|
1147
|
+
# set with the same owner/class/type in another section.
|
|
1148
|
+
msg.public_send(section).each do |rr|
|
|
1149
|
+
ValidationBudget.current&.check!(1)
|
|
1150
|
+
type = rr.type == Types.RRSIG ? rr.type_covered : rr.type
|
|
1151
|
+
limit = limits[[rr.name.canonical, rr.klass.code, type.code]]
|
|
1152
|
+
next unless limit
|
|
1153
|
+
previous = rr.instance_variable_get(:@dnsruby_authenticated_expiration)
|
|
1154
|
+
deadline = [previous, limit.last].compact.min
|
|
1155
|
+
rr.ttl = [rr.ttl, limit.first, [deadline - now, 0].max].min
|
|
1156
|
+
rr.instance_variable_set(:@dnsruby_authenticated_expiration, deadline)
|
|
1157
|
+
rr.instance_variable_set(:@dnsruby_received_at, now)
|
|
1158
|
+
end
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
private :terminal_answer, :within_zone?, :denial_bitmap?, :nsec3_denial,
|
|
1162
|
+
:nsec_denial, :apply_verified_lifetimes, :find_denial_record, :consistent_nsec_intervals?
|
|
1163
|
+
|
|
1164
|
+
def verify_ds_rrset(ds_rrset, keys = nil, msg = nil, context = {}) # :nodoc:
|
|
1165
|
+
with_validation { verify_ds_rrset_internal(ds_rrset, keys, msg, context) }
|
|
573
1166
|
end
|
|
574
1167
|
|
|
575
|
-
def
|
|
1168
|
+
private def verify_ds_rrset_internal(ds_rrset, keys, msg, context)
|
|
576
1169
|
# print "verify_ds_rrset #{ds_rrset}\n"
|
|
577
1170
|
if (ds_rrset && ds_rrset.num_sigs > 0)
|
|
578
|
-
if (verify_rrset(ds_rrset, keys))
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
found = true
|
|
589
|
-
end
|
|
590
|
-
end
|
|
591
|
-
}
|
|
592
|
-
end
|
|
593
|
-
end
|
|
594
|
-
get_keys_to_check().each {|key|
|
|
595
|
-
if (check_ds(key, ds_rrset))
|
|
596
|
-
found = true
|
|
1171
|
+
if (verify_rrset(ds_rrset, keys, context))
|
|
1172
|
+
return true if context[:insecure]
|
|
1173
|
+
provenance = context.fetch(:provenance)
|
|
1174
|
+
retain_learned_rrset(ds_rrset, provenance)
|
|
1175
|
+
if usable_provenance?(provenance)
|
|
1176
|
+
publish_trust do
|
|
1177
|
+
same_set = lambda { |set| set.name == ds_rrset.name && set.klass == ds_rrset.klass && set.type == ds_rrset.type }
|
|
1178
|
+
trust_operation[:ds].reject! { |set, _trust| same_set.call(set) }
|
|
1179
|
+
trust_operation[:ds] << [ds_rrset, provenance]
|
|
1180
|
+
@discovered_ds_store.add(ds_rrset, provenance.expiration)
|
|
597
1181
|
end
|
|
598
|
-
}
|
|
599
|
-
# If we couldn't find the trusted key, then we should store the
|
|
600
|
-
# key tag and digest in a @@discovered_ds_store.
|
|
601
|
-
# Each time we see a new key (which has been signed) then we should
|
|
602
|
-
# check if it is sitting on the discovered_ds_store.
|
|
603
|
-
# If it is, then we should add it to the trusted_keys and remove the
|
|
604
|
-
# DS from the discovered_ds_store
|
|
605
|
-
if (!found)
|
|
606
|
-
@discovered_ds_store.push(ds_rrset)
|
|
607
1182
|
end
|
|
608
|
-
# end
|
|
609
1183
|
return true
|
|
610
1184
|
else
|
|
611
1185
|
return false
|
|
@@ -614,60 +1188,116 @@ module Dnsruby
|
|
|
614
1188
|
return false # no DS rrset to verify
|
|
615
1189
|
end
|
|
616
1190
|
|
|
617
|
-
def verify_key_rrset(key_rrset, keys = nil) # :nodoc:
|
|
1191
|
+
def verify_key_rrset(key_rrset, keys = nil, context = {}) # :nodoc:
|
|
1192
|
+
with_validation { verify_key_rrset_internal(key_rrset, keys, context) }
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
private def verify_key_rrset_internal(key_rrset, keys, context)
|
|
618
1196
|
# print "verify_key_rrset\n"
|
|
619
1197
|
verified = false
|
|
620
1198
|
if (key_rrset && key_rrset.num_sigs > 0)
|
|
621
|
-
if (verify_rrset(key_rrset, keys))
|
|
1199
|
+
if (verify_rrset(key_rrset, keys, context))
|
|
622
1200
|
# key_rrset.rrs.each do |rr|
|
|
623
1201
|
# print "Adding keys : "
|
|
624
1202
|
# key_rrset.rrs.each {|rr| print "#{rr.key_tag}, "}
|
|
625
1203
|
# print "\n"
|
|
626
|
-
|
|
1204
|
+
unless context[:insecure]
|
|
1205
|
+
retain_learned_rrset(key_rrset, context.fetch(:provenance))
|
|
1206
|
+
add_trusted_key(key_rrset, context.fetch(:expiration))
|
|
1207
|
+
end
|
|
627
1208
|
verified = true
|
|
628
1209
|
end
|
|
629
|
-
check_ds_stores(key_rrset)
|
|
630
1210
|
end
|
|
631
1211
|
return verified
|
|
632
1212
|
end
|
|
633
1213
|
|
|
634
|
-
def check_ds_stores(key_rrset) # :nodoc:
|
|
635
|
-
|
|
1214
|
+
def check_ds_stores(key_rrset, signatures = nil) # :nodoc:
|
|
1215
|
+
with_validation { check_ds_stores_internal(key_rrset, signatures) }
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
private def check_ds_stores_internal(key_rrset, signatures)
|
|
1219
|
+
signatures ||= valid_signatures(key_rrset)
|
|
1220
|
+
return if signatures.empty?
|
|
1221
|
+
# A digest match supplies a candidate key, not a cache lifetime. Publish
|
|
1222
|
+
# only after authenticating the DNSKEY RRset and its TTL.
|
|
1223
|
+
verify_key_rrset(key_rrset)
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
def ds_candidate_keys(key_rrset, explicit_ds = nil)
|
|
1227
|
+
configured, discovered = @trust_mutex.synchronize do
|
|
1228
|
+
expire_trust_anchors
|
|
1229
|
+
[@configured_ds_index.matching_rrsets(key_rrset.rrs), @discovered_ds_store.matching_rrsets(key_rrset.rrs)]
|
|
1230
|
+
end
|
|
1231
|
+
stores = configured.map { |rrset| [nil, preferred_ds_records(rrset.rrs)] }
|
|
1232
|
+
stores.concat(discovered.filter_map do |rrset|
|
|
1233
|
+
provenance = learned_provenance(rrset)
|
|
1234
|
+
[admit_provenance(provenance), preferred_ds_records(rrset.rrs)] if usable_provenance?(provenance)
|
|
1235
|
+
end)
|
|
1236
|
+
stores.concat((trust_operation&.fetch(:ds) || []).filter_map do |rrset, provenance|
|
|
1237
|
+
[provenance, preferred_ds_records(rrset.rrs)] if usable_provenance?(provenance)
|
|
1238
|
+
end)
|
|
1239
|
+
if explicit_ds
|
|
1240
|
+
# Caller-supplied DS records are explicit trust input. If this is a
|
|
1241
|
+
# learned DS RRset or a new set wrapping learned records, retain their
|
|
1242
|
+
# original absolute deadlines and trust sources.
|
|
1243
|
+
provenance = admit_learned_rrset(explicit_ds)
|
|
1244
|
+
stores = preferred_ds_records(explicit_ds.rrs).filter_map do |record|
|
|
1245
|
+
trust = provenance || learned_provenance(record)
|
|
1246
|
+
next if trust && !usable_provenance?(trust)
|
|
1247
|
+
[admit_provenance(trust), [record]]
|
|
1248
|
+
end
|
|
1249
|
+
raise VerifyError, 'Learned DS trust is no longer valid' if stores.empty?
|
|
1250
|
+
end
|
|
1251
|
+
# Apply digest preference above to whole RRsets before indexing. Otherwise
|
|
1252
|
+
# an unrelated SHA-256 record could accidentally permit a SHA-1 fallback.
|
|
1253
|
+
indexed = {}
|
|
1254
|
+
stores.each do |provenance, records|
|
|
1255
|
+
records.each do |record|
|
|
1256
|
+
candidate = DSCache.candidate_identity(record)
|
|
1257
|
+
(indexed[candidate] ||= []) << [provenance, record]
|
|
1258
|
+
end
|
|
1259
|
+
end
|
|
1260
|
+
candidates = {}
|
|
636
1261
|
key_rrset.rrs.each do |key|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
@trusted_keys.add_key_with_expiration(key, key_rrset.sigs()[0].expiration)
|
|
640
|
-
end
|
|
1262
|
+
matches = [key.name.canonical, nil].flat_map do |owner|
|
|
1263
|
+
indexed[[owner, key.klass.code, key.algorithm.code, key.key_tag]] || []
|
|
641
1264
|
end
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (ds.check_key(key))
|
|
650
|
-
@trusted_keys.add_key_with_expiration(key, tbtk.sigs()[0].expiration)
|
|
651
|
-
@discovered_ds_store.delete(tbtk)
|
|
652
|
-
end
|
|
653
|
-
}
|
|
1265
|
+
matches.each do |provenance, ds|
|
|
1266
|
+
ValidationBudget.current&.check!(1)
|
|
1267
|
+
next unless ds.check_key(key)
|
|
1268
|
+
identity = KeyCache.key_identity(key)
|
|
1269
|
+
previous = candidates[identity]
|
|
1270
|
+
if !previous || (previous.last && (!provenance || provenance.expiration > previous.last.expiration))
|
|
1271
|
+
candidates[identity] = [key, provenance]
|
|
654
1272
|
end
|
|
655
1273
|
end
|
|
656
|
-
# end
|
|
657
1274
|
end
|
|
658
|
-
|
|
1275
|
+
candidates
|
|
659
1276
|
end
|
|
1277
|
+
private :ds_candidate_keys
|
|
660
1278
|
|
|
661
|
-
def get_keys_to_check # :nodoc:
|
|
662
|
-
|
|
663
|
-
|
|
1279
|
+
def get_keys_to_check(signatures = nil) # :nodoc:
|
|
1280
|
+
anchors, keys = @trust_mutex.synchronize do
|
|
1281
|
+
expire_trust_anchors
|
|
1282
|
+
[signatures ? @trust_anchors.matching_keys(signatures) : @trust_anchors.keys,
|
|
1283
|
+
cached_trust_keys(signatures)]
|
|
1284
|
+
end
|
|
1285
|
+
local = (trust_operation&.fetch(:keys)&.values || []).filter_map do |key, provenance|
|
|
1286
|
+
key if usable_provenance?(provenance)
|
|
1287
|
+
end
|
|
1288
|
+
(anchors + keys.select { |key| usable_cached_key?(key) } + local).uniq { |key| KeyCache.key_identity(key) }
|
|
664
1289
|
end
|
|
665
1290
|
|
|
666
1291
|
# Find the first matching DNSKEY and RRSIG record in the two sets.
|
|
667
1292
|
def get_matching_key(keys, sigrecs)#:nodoc: all
|
|
668
|
-
|
|
1293
|
+
each_matching_key(keys, sigrecs) { |key, sig| return key, sig }
|
|
1294
|
+
return nil, nil
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# A key tag is only a hint: every candidate must be cryptographically tried.
|
|
1298
|
+
def each_matching_key(keys, sigrecs)
|
|
669
1299
|
if ((keys == nil) || (sigrecs == nil))
|
|
670
|
-
return
|
|
1300
|
+
return
|
|
671
1301
|
end
|
|
672
1302
|
if ((RR::DNSKEY === keys) || (RR::DS === keys) ||
|
|
673
1303
|
((RR::DLV === keys) && (@verifier_type == VerifierType::DLV)))
|
|
@@ -677,27 +1307,35 @@ module Dnsruby
|
|
|
677
1307
|
if (enumerator.class == RRSet)
|
|
678
1308
|
enumerator = enumerator.rrs
|
|
679
1309
|
end
|
|
1310
|
+
signatures_by_key = sigrecs.group_by do |sig|
|
|
1311
|
+
KeyCache.candidate_identity(sig.signers_name, sig.klass, sig.algorithm, sig.key_tag)
|
|
1312
|
+
end
|
|
680
1313
|
enumerator.each {|key|
|
|
1314
|
+
ValidationBudget.current&.check!(1)
|
|
1315
|
+
next if trust_operation&.fetch(:operational, false) && !algorithm_supported?(key.algorithm, operational: true)
|
|
681
1316
|
if ((key.revoked?)) # || (key.bad_flags?))
|
|
682
1317
|
next
|
|
683
1318
|
end
|
|
684
1319
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
end
|
|
691
|
-
}
|
|
1320
|
+
candidate = KeyCache.candidate_identity(key.name, key.klass, key.algorithm, key.key_tag)
|
|
1321
|
+
(signatures_by_key[candidate] || []).each do |sig|
|
|
1322
|
+
ValidationBudget.current&.check!(1)
|
|
1323
|
+
yield key, sig
|
|
1324
|
+
end
|
|
692
1325
|
}
|
|
693
|
-
return nil, nil
|
|
694
1326
|
end
|
|
1327
|
+
private :each_matching_key
|
|
695
1328
|
|
|
696
1329
|
# Verify the signature of an rrset encoded with the specified KeyCache
|
|
697
1330
|
# or RRSet. If no signature is included, false is returned.
|
|
698
1331
|
#
|
|
699
1332
|
# Returns true if the RRSet verified, false otherwise.
|
|
700
|
-
def verify_rrset(rrset, keys = nil)
|
|
1333
|
+
def verify_rrset(rrset, keys = nil, context = {})
|
|
1334
|
+
with_validation { verify_rrset_internal(rrset, keys, context) }
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
private def verify_rrset_internal(rrset, keys, context)
|
|
1338
|
+
received_at = rrset.rrs.map { |rr| rr.instance_variable_get(:@dnsruby_received_at) }.compact.min || Time.now.to_i
|
|
701
1339
|
# print "Verify_rrset #{rrset.name}, #{rrset.type}\n"
|
|
702
1340
|
# print "ABOUT TO VERIFY WITH #{keys == nil ? '0' : keys.length} keys\n"
|
|
703
1341
|
# if (keys != nil)
|
|
@@ -712,72 +1350,119 @@ module Dnsruby
|
|
|
712
1350
|
if (rrset.num_sigs == 0)
|
|
713
1351
|
raise VerifyError.new("No signatures in the RRSet : #{rrset.name}, #{rrset.type}")
|
|
714
1352
|
end
|
|
715
|
-
sigrecs
|
|
716
|
-
check_rr_data(rrset, sigrec)
|
|
717
|
-
end
|
|
1353
|
+
sigrecs = valid_signatures(rrset)
|
|
718
1354
|
raise ArgumentError.new("Expecting DNSKEY, DLV, DS, RRSet, Array or nil for keys : got #{keys.class} instead") if
|
|
719
1355
|
(keys && (![Array, RR::IN::DNSKEY, RR::IN::DLV, RR::IN::DS].include?keys.class) && (keys.class != RRSet))
|
|
720
1356
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
@trusted_keys.add_key_with_expiration(key, rrset.sigs()[0].expiration)
|
|
729
|
-
end
|
|
730
|
-
end
|
|
731
|
-
end
|
|
732
|
-
else
|
|
733
|
-
check_ds_stores(rrset)
|
|
734
|
-
end
|
|
1357
|
+
proof_keys = keys
|
|
1358
|
+
explicit_keys = !keys.nil?
|
|
1359
|
+
explicit_provenance = admit_learned_rrset(keys)
|
|
1360
|
+
# Wrap a single DS or DLV record without losing its learned provenance.
|
|
1361
|
+
if [RR::IN::DS, RR::IN::DLV].include?(keys.class)
|
|
1362
|
+
keys = RRSet.new([keys])
|
|
1363
|
+
retain_learned_rrset(keys, explicit_provenance) if explicit_provenance
|
|
735
1364
|
end
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1365
|
+
|
|
1366
|
+
ds_keys = {}
|
|
1367
|
+
explicit_ds = keys if keys.is_a?(RRSet) &&
|
|
1368
|
+
(keys.type == Types.DS || (keys.type == Types.DLV && @verifier_type == VerifierType::DLV))
|
|
1369
|
+
if rrset.type == Types.DNSKEY && (keys.nil? || explicit_ds)
|
|
1370
|
+
ds_keys = ds_candidate_keys(rrset, explicit_ds)
|
|
1371
|
+
end
|
|
1372
|
+
if keys.nil? || explicit_ds
|
|
1373
|
+
keys = get_keys_to_check(sigrecs) + ds_keys.values.map(&:first)
|
|
740
1374
|
end
|
|
741
1375
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1376
|
+
key_members = if rrset.type == Types.DNSKEY
|
|
1377
|
+
rrset.rrs.each_with_object({}) do |key, members|
|
|
1378
|
+
ValidationBudget.current&.check!(1)
|
|
1379
|
+
members[KeyCache.key_identity(key)] = true
|
|
1380
|
+
end
|
|
746
1381
|
end
|
|
1382
|
+
error = VerifyError.new("Signing key not found")
|
|
1383
|
+
canonical_data = {}
|
|
1384
|
+
each_matching_key(keys, sigrecs) do |keyrec, sigrec|
|
|
1385
|
+
begin
|
|
1386
|
+
identity = KeyCache.key_identity(keyrec)
|
|
1387
|
+
if key_members && !key_members.key?(identity)
|
|
1388
|
+
raise VerifyError, 'Signing DNSKEY is absent from the apex DNSKEY RRSet'
|
|
1389
|
+
end
|
|
1390
|
+
parent_provenance = if explicit_ds
|
|
1391
|
+
ds_keys.key?(identity) ? ds_keys[identity].last : key_provenance(keyrec)
|
|
1392
|
+
elsif explicit_keys
|
|
1393
|
+
explicit_provenance || admit_learned_rrset(keyrec)
|
|
1394
|
+
elsif ds_keys.key?(identity)
|
|
1395
|
+
ds_keys[identity].last
|
|
1396
|
+
else
|
|
1397
|
+
key_provenance(keyrec)
|
|
1398
|
+
end
|
|
1399
|
+
next if parent_provenance && !usable_provenance?(parent_provenance)
|
|
1400
|
+
parent_provenance = admit_provenance(parent_provenance)
|
|
1401
|
+
ValidationBudget.current&.signature!
|
|
1402
|
+
ttl_expiration = received_at + [rrset.ttl, sigrec.ttl, sigrec.original_ttl].min
|
|
1403
|
+
previous_expirations = (rrset.rrs + [sigrec]).map do |rr|
|
|
1404
|
+
rr.instance_variable_get(:@dnsruby_authenticated_expiration)
|
|
1405
|
+
end.compact
|
|
1406
|
+
verify_rrset_with_key(rrset, keyrec, sigrec, canonical_data)
|
|
1407
|
+
check_wildcard_proof(rrset, sigrec, proof_keys, context) if context[:message]
|
|
1408
|
+
if parent_provenance && !usable_provenance?(parent_provenance)
|
|
1409
|
+
raise VerifyError, 'Learned trust expired during verification'
|
|
1410
|
+
end
|
|
1411
|
+
signature_expiration = SerialTime.absolute(sigrec.expiration, Time.now.to_i)
|
|
1412
|
+
raise VerifyError, 'Signature expiration is ambiguous' unless signature_expiration
|
|
1413
|
+
expiration = ([ttl_expiration, signature_expiration] + previous_expirations).min
|
|
1414
|
+
provenance = authenticated_provenance(rrset, sigrec, expiration, parent_provenance)
|
|
1415
|
+
verified_at = Time.now.to_i
|
|
1416
|
+
rrset.ttl = [rrset.ttl, [provenance.expiration - verified_at, 0].max].min
|
|
1417
|
+
rrset.each do |rr|
|
|
1418
|
+
rr.instance_variable_set(:@dnsruby_authenticated_expiration, provenance.expiration)
|
|
1419
|
+
rr.instance_variable_set(:@dnsruby_received_at, verified_at)
|
|
1420
|
+
end
|
|
1421
|
+
context[:expiration] = provenance.expiration
|
|
1422
|
+
context[:provenance] = provenance
|
|
1423
|
+
context[:signature] = sigrec
|
|
1424
|
+
context[:verified][rrset] = { signature: sigrec, expiration: provenance.expiration } if context[:verified]
|
|
1425
|
+
return true
|
|
1426
|
+
rescue VerifyError => e
|
|
1427
|
+
error = e
|
|
1428
|
+
rescue OpenSSL::OpenSSLError => e
|
|
1429
|
+
# Wire-decoded DNSKEYs may only be parsed by OpenSSL at this point.
|
|
1430
|
+
# A malformed colliding candidate must not mask a valid signing key.
|
|
1431
|
+
error = VerifyError.new("Signature failed to cryptographically verify: #{e.message}")
|
|
1432
|
+
end
|
|
1433
|
+
end
|
|
1434
|
+
raise error
|
|
1435
|
+
end
|
|
1436
|
+
|
|
1437
|
+
def algorithm_supported?(algorithm, operational: false)
|
|
1438
|
+
allowed = operational ? OPERATIONAL_ALGORITHMS : VALIDATION_ALGORITHMS
|
|
1439
|
+
allowed.include?(algorithm) &&
|
|
1440
|
+
(algorithm != Algorithms.ED25519 || RR::DNSKEY.ed25519_supported?) &&
|
|
1441
|
+
(algorithm != Algorithms.ED448 || RR::DNSKEY.ed448_supported?)
|
|
1442
|
+
end
|
|
1443
|
+
private :algorithm_supported?
|
|
747
1444
|
|
|
1445
|
+
def verify_rrset_with_key(rrset, keyrec, sigrec, canonical_data = {})
|
|
748
1446
|
# RFC 4034
|
|
749
1447
|
# 3.1.8.1. Signature Calculation
|
|
750
1448
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
# return false
|
|
754
|
-
raise VerifyError.new("DNSKEY with SEP flag set and Zone Key flag not set")
|
|
1449
|
+
unless keyrec.zone_key?
|
|
1450
|
+
raise VerifyError, 'DNSKEY Zone Key flag is not set'
|
|
755
1451
|
end
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
# Any DNS names in the RDATA field of each RR MUST be in
|
|
761
|
-
# canonical form; and
|
|
762
|
-
# The RRset MUST be sorted in canonical order.
|
|
763
|
-
rrset = rrset.sort_canonical
|
|
764
|
-
|
|
765
|
-
sig_data = sigrec.sig_data
|
|
766
|
-
|
|
767
|
-
# RR(i) = owner | type | class | TTL | RDATA length | RDATA
|
|
768
|
-
rrset.each do |rec|
|
|
769
|
-
old_ttl = rec.ttl
|
|
770
|
-
rec.ttl = sigrec.original_ttl
|
|
771
|
-
data = MessageEncoder.new { |msg|
|
|
772
|
-
msg.put_rr(rec, true)
|
|
773
|
-
}.to_s # @TODO@ worry about wildcards here?
|
|
774
|
-
rec.ttl = old_ttl
|
|
775
|
-
if (RUBY_VERSION >= "1.9")
|
|
776
|
-
data.force_encoding(Encoding::BINARY)
|
|
1452
|
+
unless algorithm_supported?(sigrec.algorithm)
|
|
1453
|
+
if [Algorithms.DSA, Algorithms.DSA_NSEC3_SHA1].include?(sigrec.algorithm)
|
|
1454
|
+
raise VerifyError, "Algorithm #{sigrec.algorithm.code} is prohibited for DNSSEC validation"
|
|
777
1455
|
end
|
|
778
|
-
|
|
1456
|
+
raise VerifyError, "Algorithm #{sigrec.algorithm.code} unsupported by Dnsruby"
|
|
779
1457
|
end
|
|
780
1458
|
|
|
1459
|
+
# Canonical RDATA is independent of the signature. The RR wire body only
|
|
1460
|
+
# varies with Original TTL and wildcard label count (RFC 4034 section 3.1.8).
|
|
1461
|
+
# Keep it local to this verification so mutable caller records are never
|
|
1462
|
+
# cached across calls, and do not temporarily mutate their TTLs.
|
|
1463
|
+
sig_data = sigrec.sig_data + canonical_rrset_data(rrset, sigrec, canonical_data)
|
|
1464
|
+
ValidationBudget.current&.check!(sig_data.bytesize / 64 + 1)
|
|
1465
|
+
|
|
781
1466
|
# Now calculate the signature
|
|
782
1467
|
verified = false
|
|
783
1468
|
if [Algorithms.RSASHA1,
|
|
@@ -787,20 +1472,17 @@ module Dnsruby
|
|
|
787
1472
|
verified = keyrec.public_key.verify(OpenSSL::Digest::SHA256.new, sigrec.signature, sig_data)
|
|
788
1473
|
elsif (sigrec.algorithm == Algorithms.RSASHA512)
|
|
789
1474
|
verified = keyrec.public_key.verify(OpenSSL::Digest::SHA512.new, sigrec.signature, sig_data)
|
|
790
|
-
elsif [Algorithms.
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
s = RR::get_num(sigrec.signature[21, 20])
|
|
797
|
-
r_asn1 = OpenSSL::ASN1::Integer.new(r)
|
|
798
|
-
s_asn1 = OpenSSL::ASN1::Integer.new(s)
|
|
799
|
-
|
|
800
|
-
asn1 = OpenSSL::ASN1::Sequence.new([r_asn1, s_asn1]).to_der
|
|
801
|
-
verified = keyrec.public_key.verify(OpenSSL::Digest::DSS1.new, asn1, sig_data)
|
|
1475
|
+
elsif [Algorithms.ED25519, Algorithms.ED448].include?(sigrec.algorithm)
|
|
1476
|
+
size = sigrec.algorithm == Algorithms.ED25519 ? 64 : 114
|
|
1477
|
+
unless sigrec.signature.bytesize == size
|
|
1478
|
+
raise VerifyError, "#{sigrec.algorithm} signature must contain #{size} octets"
|
|
1479
|
+
end
|
|
1480
|
+
verified = keyrec.public_key.verify(nil, sigrec.signature, sig_data)
|
|
802
1481
|
elsif [Algorithms.ECDSAP256SHA256, Algorithms.ECDSAP384SHA384].include?(sigrec.algorithm)
|
|
803
1482
|
byte_size = (keyrec.public_key.group.degree + 7) / 8
|
|
1483
|
+
if sigrec.signature.bytesize != 2 * byte_size
|
|
1484
|
+
raise VerifyError, "Invalid ECDSA signature length"
|
|
1485
|
+
end
|
|
804
1486
|
sig_bytes = sigrec.signature[0..(byte_size - 1)]
|
|
805
1487
|
sig_char = sigrec.signature[byte_size..-1] || ''
|
|
806
1488
|
asn1 = OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
|
@@ -813,21 +1495,161 @@ module Dnsruby
|
|
|
813
1495
|
|
|
814
1496
|
verified = keyrec.public_key.dsa_verify_asn1(digest_obj.digest(sig_data), asn1)
|
|
815
1497
|
else
|
|
816
|
-
raise
|
|
1498
|
+
raise VerifyError.new("Algorithm #{sigrec.algorithm.code} unsupported by Dnsruby")
|
|
817
1499
|
end
|
|
818
1500
|
|
|
819
1501
|
if (!verified)
|
|
820
1502
|
raise VerifyError.new("Signature failed to cryptographically verify")
|
|
821
1503
|
end
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1504
|
+
ValidationBudget.current&.check!
|
|
1505
|
+
# Commit lifetime bounds only after the wildcard proof also succeeds.
|
|
1506
|
+
unless SerialTime.valid?(sigrec.inception, sigrec.expiration)
|
|
1507
|
+
raise VerifyError, 'Signature expired during verification'
|
|
1508
|
+
end
|
|
826
1509
|
# print "VERIFIED OK\n"
|
|
827
1510
|
return true
|
|
828
1511
|
end
|
|
1512
|
+
private :verify_rrset_with_key
|
|
1513
|
+
|
|
1514
|
+
def canonical_rrset_data(rrset, signature, cache)
|
|
1515
|
+
identity = [signature.original_ttl, signature.labels]
|
|
1516
|
+
return cache[identity] if cache.key?(identity)
|
|
1517
|
+
name = rrset.name
|
|
1518
|
+
if signature.labels < 0 || signature.labels > name.labels.length
|
|
1519
|
+
raise VerifyError, 'Invalid RRSIG label count'
|
|
1520
|
+
elsif signature.labels < name.labels.length
|
|
1521
|
+
name = Name.new([Name::Label.new('*')] + name.labels.last(signature.labels), true)
|
|
1522
|
+
end
|
|
1523
|
+
rdatas = cache[:rdatas] ||= rrset.rrs.map do |record|
|
|
1524
|
+
ValidationBudget.current&.check!(1)
|
|
1525
|
+
data = MessageEncoder.new { |encoder| record.encode_rdata(encoder, true) }.to_s
|
|
1526
|
+
ValidationBudget.current&.check!(data.bytesize / 64 + 1)
|
|
1527
|
+
data
|
|
1528
|
+
end.uniq.sort
|
|
1529
|
+
prefix = MessageEncoder.new do |encoder|
|
|
1530
|
+
encoder.put_name(name, true)
|
|
1531
|
+
encoder.put_pack('nnN', rrset.type.code, rrset.klass.code, signature.original_ttl)
|
|
1532
|
+
end.to_s
|
|
1533
|
+
cache[identity] = rdatas.map do |data|
|
|
1534
|
+
ValidationBudget.current&.check!(1)
|
|
1535
|
+
prefix + [data.bytesize].pack('n') + data
|
|
1536
|
+
end.join
|
|
1537
|
+
end
|
|
1538
|
+
private :canonical_rrset_data
|
|
1539
|
+
|
|
1540
|
+
# A valid wildcard signature authenticates the source wildcard, not the
|
|
1541
|
+
# expanded owner. RFC 4035 5.3.4 / RFC 5155 8.8 also require denial of the
|
|
1542
|
+
# next-closer name. Keep this at message level: verify_rrset alone has no
|
|
1543
|
+
# response from which to obtain that proof.
|
|
1544
|
+
def check_wildcard_proof(rrset, signature, keys, context)
|
|
1545
|
+
labels = signature.labels
|
|
1546
|
+
return if labels == rrset.name.labels.length
|
|
1547
|
+
return if rrset.name.wild? && labels == rrset.name.labels.length - 1
|
|
1548
|
+
zone = signature.signers_name
|
|
1549
|
+
closest = Name.new(rrset.name.labels.last(labels), true)
|
|
1550
|
+
next_closer = Name.new(rrset.name.labels.last(labels + 1), true)
|
|
1551
|
+
unless closest == zone || closest.subdomain_of?(zone)
|
|
1552
|
+
raise VerifyError, 'Wildcard source is outside the signing zone'
|
|
1553
|
+
end
|
|
1554
|
+
proof_id = [zone.canonical, rrset.klass.code, next_closer.canonical]
|
|
1555
|
+
if (cached = context[:proofs][proof_id])
|
|
1556
|
+
context[:insecure] = true if cached == :insecure
|
|
1557
|
+
return
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
authority = context[:sections].fetch('authority')
|
|
1561
|
+
insecure_proof = false
|
|
1562
|
+
authority.each do |proof|
|
|
1563
|
+
ValidationBudget.current&.check!(1)
|
|
1564
|
+
next unless [Types.NSEC, Types.NSEC3].include?(proof.type)
|
|
1565
|
+
next unless proof.klass == rrset.klass
|
|
1566
|
+
covers = proof.rrs.any? do |record|
|
|
1567
|
+
if record.type == Types.NSEC
|
|
1568
|
+
nsec_covers_next_closer?(record, next_closer, zone)
|
|
1569
|
+
else
|
|
1570
|
+
nsec3_covers_next_closer?(record, next_closer, zone)
|
|
1571
|
+
end
|
|
1572
|
+
end
|
|
1573
|
+
next unless covers
|
|
1574
|
+
# Only signatures from the wildcard's zone can authenticate its proof.
|
|
1575
|
+
# An expanded NSEC itself cannot prove the absence of its new owner.
|
|
1576
|
+
signatures = proof.sigs.select do |sig|
|
|
1577
|
+
sig.signers_name == zone && (sig.labels == proof.name.labels.length ||
|
|
1578
|
+
(proof.name.wild? && sig.labels == proof.name.labels.length - 1))
|
|
1579
|
+
end
|
|
1580
|
+
next if signatures.empty?
|
|
1581
|
+
begin
|
|
1582
|
+
verified = context[:verified][proof]
|
|
1583
|
+
unless verified && signatures.include?(verified[:signature])
|
|
1584
|
+
proof_context = {}
|
|
1585
|
+
restricted = RRSet.new(proof.rrs + signatures)
|
|
1586
|
+
verify_rrset(restricted, keys, proof_context)
|
|
1587
|
+
proof.ttl = [proof.ttl, restricted.ttl].min
|
|
1588
|
+
context[:verified][proof] = {
|
|
1589
|
+
signature: proof_context.fetch(:signature),
|
|
1590
|
+
expiration: proof_context.fetch(:expiration)
|
|
1591
|
+
}
|
|
1592
|
+
end
|
|
1593
|
+
if proof.type == Types.NSEC3 && proof.rrs.any? { |rr| rr.usable_hash? && rr.flags == 1 }
|
|
1594
|
+
insecure_proof = true
|
|
1595
|
+
next
|
|
1596
|
+
end
|
|
1597
|
+
context[:proofs][proof_id] = :secure
|
|
1598
|
+
return
|
|
1599
|
+
rescue VerifyError
|
|
1600
|
+
# Another authenticated denial record may supply the proof.
|
|
1601
|
+
end
|
|
1602
|
+
end
|
|
1603
|
+
if insecure_proof
|
|
1604
|
+
context[:proofs][proof_id] = :insecure
|
|
1605
|
+
context[:insecure] = true
|
|
1606
|
+
return
|
|
1607
|
+
end
|
|
1608
|
+
raise VerifyError, "No authenticated wildcard denial proof for #{rrset.name}"
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1611
|
+
def nsec_covers_next_closer?(record, name, zone)
|
|
1612
|
+
owner, following = record.name, record.next_domain
|
|
1613
|
+
return false unless [owner, following].all? { |n| n == zone || n.subdomain_of?(zone) }
|
|
1614
|
+
# A descendant proves an empty nonterminal exists. Parent-side denial
|
|
1615
|
+
# below a delegation or DNAME cannot establish absence in that subtree.
|
|
1616
|
+
return false if following.subdomain_of?(name) || owner.subdomain_of?(name)
|
|
1617
|
+
if name.subdomain_of?(owner)
|
|
1618
|
+
return false if record.types.include?(Types.DNAME) ||
|
|
1619
|
+
(record.types.include?(Types.NS) && !record.types.include?(Types.SOA))
|
|
1620
|
+
end
|
|
1621
|
+
interval_covers?(owner, following, name)
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1624
|
+
def nsec3_covers_next_closer?(record, name, zone)
|
|
1625
|
+
return false unless record.name.strip_label == zone && record.hash_alg == Nsec3HashAlgorithms.SHA_1
|
|
1626
|
+
# An Opt-Out interval can hide an insecure delegation, so cannot prove
|
|
1627
|
+
# this wildcard expansion secure (RFC 5155 section 9.2).
|
|
1628
|
+
return false unless [0, 1].include?(record.flags) && record.next_hashed.bytesize == 20
|
|
1629
|
+
owner = record.name.labels.first.to_s.downcase
|
|
1630
|
+
return false unless /\A[0-9a-v]{32}\z/.match?(owner)
|
|
1631
|
+
following = RR::NSEC3.encode_next_hashed(record.next_hashed).downcase
|
|
1632
|
+
hashed = RR::NSEC3.calculate_hash(name, record.iterations, RR::NSEC3.decode_salt(record.salt), record.hash_alg)
|
|
1633
|
+
interval_covers?(owner, following, hashed)
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
def interval_covers?(first, following, name)
|
|
1637
|
+
return false if name == first || name == following
|
|
1638
|
+
if first.is_a?(Name)
|
|
1639
|
+
first.canonically_before(following) ?
|
|
1640
|
+
first.canonically_before(name) && name.canonically_before(following) :
|
|
1641
|
+
first.canonically_before(name) || name.canonically_before(following)
|
|
1642
|
+
else
|
|
1643
|
+
first < following ? first < name && name < following : first < name || name < following
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
private :check_wildcard_proof, :nsec_covers_next_closer?, :nsec3_covers_next_closer?, :interval_covers?
|
|
829
1647
|
|
|
830
1648
|
def find_closest_dlv_anchor_for(name) # :nodoc:
|
|
1649
|
+
with_validation { find_closest_dlv_anchor_for_internal(name) }
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
private def find_closest_dlv_anchor_for_internal(name)
|
|
831
1653
|
# To find the closest anchor, query DLV.isc.org for [a.b.c.d], then [a.b.c], [a.b], etc.
|
|
832
1654
|
# once closest anchor found, simply run follow_chain from that anchor
|
|
833
1655
|
|
|
@@ -850,6 +1672,10 @@ module Dnsruby
|
|
|
850
1672
|
end
|
|
851
1673
|
|
|
852
1674
|
def get_zone_key_from_dlv_rrset(dlv_rrset, name) # :nodoc:
|
|
1675
|
+
with_validation { get_zone_key_from_dlv_rrset_internal(dlv_rrset, name) }
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
private def get_zone_key_from_dlv_rrset_internal(dlv_rrset, name)
|
|
853
1679
|
# We want to return the key for the zone i.e. DS/DNSKEY for .se, NOT DLV for se.dlv.isc.org
|
|
854
1680
|
# So, we have the DLv record. Now use it to add the zone's DNSKEYs to the trusted key set.
|
|
855
1681
|
res = get_nameservers_for(name)
|
|
@@ -874,6 +1700,8 @@ module Dnsruby
|
|
|
874
1700
|
if (!ret)
|
|
875
1701
|
raise ResolvError.new("Couldn't get DNSKEY from Recursor")
|
|
876
1702
|
end
|
|
1703
|
+
rescue RecursionLimitError
|
|
1704
|
+
raise
|
|
877
1705
|
rescue ResolvError => e
|
|
878
1706
|
# print "Error getting zone key from DLV RR for #{name} : #{e}\n"
|
|
879
1707
|
TheLog.error("Error getting zone key from DLV RR for #{name} : #{e}")
|
|
@@ -892,6 +1720,10 @@ module Dnsruby
|
|
|
892
1720
|
end
|
|
893
1721
|
|
|
894
1722
|
def query_dlv_for(name) # :nodoc:
|
|
1723
|
+
with_validation { query_dlv_for_internal(name) }
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
private def query_dlv_for_internal(name)
|
|
895
1727
|
# See if there is a record for name in dlv.isc.org
|
|
896
1728
|
if (!@res && (@verifier_type == VerifierType::DLV))
|
|
897
1729
|
@res = get_dlv_resolver
|
|
@@ -908,6 +1740,8 @@ module Dnsruby
|
|
|
908
1740
|
if (!ret)
|
|
909
1741
|
raise ResolvError.new("Couldn't get DLV record from Recursor")
|
|
910
1742
|
end
|
|
1743
|
+
rescue RecursionLimitError
|
|
1744
|
+
raise
|
|
911
1745
|
rescue ResolvError => e
|
|
912
1746
|
# print "Error getting DLV record for #{name} : #{e}\n"
|
|
913
1747
|
TheLog.info("Error getting DLV record for #{name} : #{e}")
|
|
@@ -932,22 +1766,40 @@ module Dnsruby
|
|
|
932
1766
|
end
|
|
933
1767
|
|
|
934
1768
|
def find_closest_anchor_for(name) # :nodoc:
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
break if (n.to_s == root.to_s)
|
|
947
|
-
# strip the name
|
|
948
|
-
n = n.strip_label
|
|
1769
|
+
n = Name.create(name == "" ? "." : name)
|
|
1770
|
+
names = [n]
|
|
1771
|
+
names << names.last.strip_label until names.last.labels.empty?
|
|
1772
|
+
# Lookup by owner before consulting provenance. A cache of unrelated
|
|
1773
|
+
# zones should not add work to every step down a delegation chain.
|
|
1774
|
+
candidates = @trust_mutex.synchronize do
|
|
1775
|
+
expire_trust_anchors
|
|
1776
|
+
names.map do |owner|
|
|
1777
|
+
[@trust_anchors.keys_for_name(owner) + @configured_ds_index.rrsets_for_name(owner),
|
|
1778
|
+
@trusted_keys.keys_for_name(owner), @discovered_ds_store.rrsets_for_name(owner)]
|
|
1779
|
+
end
|
|
949
1780
|
end
|
|
950
|
-
|
|
1781
|
+
operation = trust_operation
|
|
1782
|
+
names.zip(candidates).each do |owner, (configured, keys, discovered)|
|
|
1783
|
+
canonical = owner.canonical
|
|
1784
|
+
anchors = configured + keys.select { |key| usable_cached_key?(key) } +
|
|
1785
|
+
discovered.select { |rrset| usable_provenance?(learned_provenance(rrset)) }
|
|
1786
|
+
if operation
|
|
1787
|
+
operation[:keys].each_value do |key, trust|
|
|
1788
|
+
anchors << key if key.name.canonical == canonical && usable_provenance?(trust)
|
|
1789
|
+
end
|
|
1790
|
+
operation[:ds].each do |rrset, trust|
|
|
1791
|
+
anchors << rrset if rrset.name.canonical == canonical && usable_provenance?(trust)
|
|
1792
|
+
end
|
|
1793
|
+
end
|
|
1794
|
+
if operation&.fetch(:operational, false)
|
|
1795
|
+
anchors.reject! { |key| key.type == Types.DNSKEY && !algorithm_supported?(key.algorithm, operational: true) }
|
|
1796
|
+
end
|
|
1797
|
+
# Records remain mutable; an index entry must not grant trust under
|
|
1798
|
+
# the old owner after the caller changes the record's name.
|
|
1799
|
+
anchor = anchors.find { |key| key.name.canonical == canonical }
|
|
1800
|
+
return anchor if anchor
|
|
1801
|
+
end
|
|
1802
|
+
false
|
|
951
1803
|
end
|
|
952
1804
|
|
|
953
1805
|
# @TODO@ Handle REVOKED keys! (RFC 5011)
|
|
@@ -956,6 +1808,19 @@ module Dnsruby
|
|
|
956
1808
|
# that authority and remove any keys with the pre-revoked key_tag.
|
|
957
1809
|
|
|
958
1810
|
def follow_chain(anchor, name) # :nodoc:
|
|
1811
|
+
with_validation do
|
|
1812
|
+
operation = trust_operation
|
|
1813
|
+
operation[:last_insecure_expiration] = nil
|
|
1814
|
+
result = follow_chain_internal(anchor, name)
|
|
1815
|
+
if !result && operation[:insecurity_expirations]
|
|
1816
|
+
operation[:insecurity_expirations] << operation[:last_insecure_expiration]
|
|
1817
|
+
end
|
|
1818
|
+
result
|
|
1819
|
+
end
|
|
1820
|
+
end
|
|
1821
|
+
|
|
1822
|
+
private def follow_chain_internal(anchor, name)
|
|
1823
|
+
admit_learned_rrset(anchor)
|
|
959
1824
|
# Follow the chain from the anchor to name, returning the appropriate
|
|
960
1825
|
# key at the end, or false.
|
|
961
1826
|
#
|
|
@@ -972,6 +1837,7 @@ module Dnsruby
|
|
|
972
1837
|
res = Dnssec.default_resolver
|
|
973
1838
|
# while ((next_step != name) || (next_key.type != Types.DNSKEY))
|
|
974
1839
|
while (true)
|
|
1840
|
+
ValidationBudget.current&.check!(1)
|
|
975
1841
|
# print "In loop for parent=#{parent}, next step = #{next_step}\n"
|
|
976
1842
|
dont_move_on = false
|
|
977
1843
|
if (next_key.type != Types.DNSKEY)
|
|
@@ -985,7 +1851,7 @@ module Dnsruby
|
|
|
985
1851
|
return false if (!next_key)
|
|
986
1852
|
# Add the next label on
|
|
987
1853
|
if (!dont_move_on)
|
|
988
|
-
parent =
|
|
1854
|
+
parent = next_key.name
|
|
989
1855
|
next_step = Name.new(name.labels[name.labels.length-1-next_step.labels.length,1] +
|
|
990
1856
|
next_step.labels , name.absolute?)
|
|
991
1857
|
# print "Next parent = #{parent}, next_step = #{next_step}, next_key.type = #{next_key.type.string}\n"
|
|
@@ -998,6 +1864,21 @@ module Dnsruby
|
|
|
998
1864
|
end
|
|
999
1865
|
|
|
1000
1866
|
def get_anchor_for(child, parent, current_anchor, parent_res = nil) # :nodoc:
|
|
1867
|
+
with_validation { get_anchor_for_internal(child, parent, current_anchor, parent_res) }
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
private def get_anchor_for_internal(child, parent, current_anchor, parent_res)
|
|
1871
|
+
admit_learned_rrset(current_anchor)
|
|
1872
|
+
if current_anchor.type == Types.DS
|
|
1873
|
+
records = current_anchor.is_a?(RRSet) ? current_anchor.rrs : [current_anchor]
|
|
1874
|
+
# A cached or configured DS is already trusted input, but its learned
|
|
1875
|
+
# provenance must still be live before its algorithm policy is used.
|
|
1876
|
+
unless preferred_ds_records(records).any? { |record| supported_ds?(record) }
|
|
1877
|
+
trust_operation[:last_insecure_expiration] = learned_provenance(current_anchor)&.expiration
|
|
1878
|
+
return false, parent_res
|
|
1879
|
+
end
|
|
1880
|
+
end
|
|
1881
|
+
ds_rrset = current_anchor
|
|
1001
1882
|
# print "Trying to discover anchor for #{child} from #{parent}\n"
|
|
1002
1883
|
TheLog.debug("Trying to discover anchor for #{child} from #{parent} using #{current_anchor}, #{parent_res}")
|
|
1003
1884
|
# We wish to return a DNSKEY which the caller can use to verify name
|
|
@@ -1031,7 +1912,6 @@ module Dnsruby
|
|
|
1031
1912
|
parent_res.dnssec = true
|
|
1032
1913
|
end
|
|
1033
1914
|
# Use that Resolver to query for DS record and NS for children
|
|
1034
|
-
ds_rrset = current_anchor
|
|
1035
1915
|
if (current_anchor.type == Types.DNSKEY)
|
|
1036
1916
|
# print "Trying to find DS records for #{child} from servers for #{parent}\n"
|
|
1037
1917
|
TheLog.debug("Trying to find DS records for #{child} from servers for #{parent}")
|
|
@@ -1041,23 +1921,36 @@ module Dnsruby
|
|
|
1041
1921
|
if (!ds_ret)
|
|
1042
1922
|
raise ResolvError.new("Couldn't get DS records from Recursor")
|
|
1043
1923
|
end
|
|
1924
|
+
rescue NXDomain => e
|
|
1925
|
+
# NXDOMAIN is usable only with its authenticated denial response.
|
|
1926
|
+
ds_ret = e.response
|
|
1927
|
+
raise VerifyError, "Missing DS denial response for #{child}" unless ds_ret
|
|
1928
|
+
rescue RecursionLimitError
|
|
1929
|
+
raise
|
|
1044
1930
|
rescue ResolvError => e
|
|
1045
|
-
|
|
1046
|
-
TheLog.error("Error getting DS record for #{child} : #{e}")
|
|
1047
|
-
return false, nil
|
|
1931
|
+
raise VerifyError, "Error getting DS record for #{child}: #{e}"
|
|
1048
1932
|
end
|
|
1049
1933
|
ds_rrset = ds_ret.answer.rrset(child, Types.DS)
|
|
1050
1934
|
if (ds_rrset.rrs.length == 0)
|
|
1051
|
-
|
|
1052
|
-
#
|
|
1053
|
-
#
|
|
1935
|
+
denial = authenticated_ds_absence(ds_ret, child, current_anchor)
|
|
1936
|
+
# A label need not be a zone cut. Continue using the current
|
|
1937
|
+
# zone's key unless the proof establishes an unsigned delegation.
|
|
1938
|
+
return denial == :insecure ? [false, parent_res] : [current_anchor, parent_res]
|
|
1054
1939
|
else
|
|
1055
1940
|
begin
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1941
|
+
unless verify(ds_rrset, current_anchor) || verify(ds_rrset)
|
|
1942
|
+
raise VerifyError, "Unsigned DS RRSet for #{child}"
|
|
1943
|
+
end
|
|
1944
|
+
# RFC 4035 5.2: a securely authenticated delegation whose DS
|
|
1945
|
+
# records all use unavailable algorithms/digests is insecure.
|
|
1946
|
+
# Mixed sets must still establish trust using a supported DS.
|
|
1947
|
+
unless preferred_ds_records(ds_rrset.rrs).any? { |ds| supported_ds?(ds) }
|
|
1948
|
+
trust_operation[:last_insecure_expiration] = learned_provenance(ds_rrset)&.expiration
|
|
1949
|
+
return false, parent_res
|
|
1950
|
+
end
|
|
1951
|
+
# Try the child nameservers only after authenticating its DS.
|
|
1952
|
+
unless Dnssec.do_validation_with_recursor?
|
|
1953
|
+
child_res = get_nameservers_from_message(child, ds_ret)
|
|
1061
1954
|
end
|
|
1062
1955
|
rescue VerifyError => e
|
|
1063
1956
|
# print "FAILED TO VERIFY DS RRSET FOR #{child}\n"
|
|
@@ -1102,39 +1995,24 @@ module Dnsruby
|
|
|
1102
1995
|
if (!key_ret)
|
|
1103
1996
|
raise ResolvError.new("Couldn't get info from Recursor")
|
|
1104
1997
|
end
|
|
1998
|
+
rescue RecursionLimitError
|
|
1999
|
+
raise
|
|
1105
2000
|
rescue ResolvError => e
|
|
1106
2001
|
# print "Error getting DNSKEY for #{child} : #{e}\n"
|
|
1107
2002
|
TheLog.error("Error getting DNSKEY for #{child} : #{e}")
|
|
1108
2003
|
# return false, nil
|
|
1109
2004
|
raise VerifyError.new("Error getting DNSKEY for #{child} : #{e}")
|
|
1110
2005
|
end
|
|
1111
|
-
verified = true
|
|
1112
2006
|
key_rrset = key_ret.answer.rrset(child, Types.DNSKEY)
|
|
1113
|
-
if
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
#
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
#
|
|
1121
|
-
|
|
1122
|
-
if (!k.zone_key?)
|
|
1123
|
-
# print "Discovered DNSKEY is not a zone key - ignoring\n"
|
|
1124
|
-
TheLog.debug("Discovered DNSKEY is not a zone key - ignoring")
|
|
1125
|
-
return false, child_res
|
|
1126
|
-
end
|
|
1127
|
-
}
|
|
1128
|
-
begin
|
|
1129
|
-
verify(key_rrset, ds_rrset)
|
|
1130
|
-
rescue VerifyError => e
|
|
1131
|
-
begin
|
|
1132
|
-
verify(key_rrset)
|
|
1133
|
-
rescue VerifyError =>e
|
|
1134
|
-
verified = false
|
|
1135
|
-
raise VerifyError.new("Couldn't verify DNSKEY and DS records")
|
|
1136
|
-
end
|
|
1137
|
-
end
|
|
2007
|
+
if key_rrset.rrs.empty?
|
|
2008
|
+
raise VerifyError, "Missing DNSKEY RRSet for #{child}"
|
|
2009
|
+
end
|
|
2010
|
+
begin
|
|
2011
|
+
raise VerifyError, "Unsigned DNSKEY RRSet for #{child}" unless verify(key_rrset, ds_rrset)
|
|
2012
|
+
rescue VerifyError
|
|
2013
|
+
# Another explicitly configured or already learned anchor can still
|
|
2014
|
+
# authenticate this same zone. Non-zone keys are ignored as signers.
|
|
2015
|
+
raise VerifyError, "Couldn't verify DNSKEY and DS records for #{child}" unless verify(key_rrset)
|
|
1138
2016
|
end
|
|
1139
2017
|
|
|
1140
2018
|
# Try to make the resolver from the authority/additional NS RRSets in DNSKEY response
|
|
@@ -1142,11 +2020,6 @@ module Dnsruby
|
|
|
1142
2020
|
if (!new_res)
|
|
1143
2021
|
new_res = child_res
|
|
1144
2022
|
end
|
|
1145
|
-
if (!verified)
|
|
1146
|
-
TheLog.info("Failed to verify DNSKEY for #{child}")
|
|
1147
|
-
return false, nil # new_res
|
|
1148
|
-
# raise VerifyError.new("Failed to verify DNSKEY for #{child}")
|
|
1149
|
-
end
|
|
1150
2023
|
# Cache.add(key_ret)
|
|
1151
2024
|
return key_rrset, new_res
|
|
1152
2025
|
rescue VerifyError => e
|
|
@@ -1157,6 +2030,67 @@ module Dnsruby
|
|
|
1157
2030
|
end
|
|
1158
2031
|
end
|
|
1159
2032
|
|
|
2033
|
+
def supported_ds?(record)
|
|
2034
|
+
algorithm_supported?(record.algorithm, operational: true) && [1, 2, 4].include?(record.digest_type.code)
|
|
2035
|
+
end
|
|
2036
|
+
private :supported_ds?
|
|
2037
|
+
|
|
2038
|
+
# Apply digest preference to the complete authenticated/trusted RRset,
|
|
2039
|
+
# before matching individual keys (RFC 4509 section 3).
|
|
2040
|
+
def preferred_ds_records(records)
|
|
2041
|
+
return records unless records.any? { |record| record.digest_type.code == 2 }
|
|
2042
|
+
records.reject { |record| record.digest_type.code == 1 }
|
|
2043
|
+
end
|
|
2044
|
+
private :preferred_ds_records
|
|
2045
|
+
|
|
2046
|
+
# Authenticate denial against the current parent key before classifying
|
|
2047
|
+
# an unsigned delegation. A successful denial at an ordinary name simply
|
|
2048
|
+
# means there is no zone cut at this label (RFC 4035 5.2 / RFC 5155 8.9).
|
|
2049
|
+
def authenticated_ds_absence(message, child, anchor)
|
|
2050
|
+
question = message.question.first
|
|
2051
|
+
unless question && question.qname == child && question.qtype == Types.DS &&
|
|
2052
|
+
question.qclass == anchor.klass && [RCode.NOERROR, RCode.NXDOMAIN].include?(message.rcode)
|
|
2053
|
+
raise VerifyError, "Unexpected DS denial response for #{child}"
|
|
2054
|
+
end
|
|
2055
|
+
zone = anchor.name
|
|
2056
|
+
groups = {}
|
|
2057
|
+
expirations = {}
|
|
2058
|
+
(message.authority.rrsets(Types.NSEC) + message.authority.rrsets(Types.NSEC3)).each do |proof|
|
|
2059
|
+
ValidationBudget.current&.check!(1)
|
|
2060
|
+
next unless proof.klass == anchor.klass
|
|
2061
|
+
signatures = proof.sigs.select do |signature|
|
|
2062
|
+
signature.signers_name == zone && (signature.labels == proof.name.labels.length ||
|
|
2063
|
+
(proof.name.wild? && signature.labels == proof.name.labels.length - 1))
|
|
2064
|
+
end
|
|
2065
|
+
next if signatures.empty?
|
|
2066
|
+
begin
|
|
2067
|
+
verification = {}
|
|
2068
|
+
verify_rrset(RRSet.new(proof.rrs + signatures), anchor, verification)
|
|
2069
|
+
(groups[proof.type] ||= []).concat(proof.rrs)
|
|
2070
|
+
(expirations[proof.type] ||= []) << verification[:expiration]
|
|
2071
|
+
rescue VerifyError
|
|
2072
|
+
# A second proof/signature may still provide valid denial.
|
|
2073
|
+
end
|
|
2074
|
+
end
|
|
2075
|
+
groups.each do |type, records|
|
|
2076
|
+
check = type == Types.NSEC3 ? :nsec3_denial : :nsec_denial
|
|
2077
|
+
if message.rcode == RCode.NOERROR &&
|
|
2078
|
+
send(check, records, child, Types.DS, zone, message.rcode, true) == :insecure
|
|
2079
|
+
operation = trust_operation
|
|
2080
|
+
operation[:last_insecure_expiration] = expirations.fetch(type).compact.min if operation
|
|
2081
|
+
return :insecure
|
|
2082
|
+
end
|
|
2083
|
+
result = send(check, records, child, Types.DS, zone, message.rcode, false)
|
|
2084
|
+
if result == :insecure
|
|
2085
|
+
operation = trust_operation
|
|
2086
|
+
operation[:last_insecure_expiration] = expirations.fetch(type).compact.min if operation
|
|
2087
|
+
end
|
|
2088
|
+
return result if result
|
|
2089
|
+
end
|
|
2090
|
+
raise VerifyError, "No authenticated DS denial proof for #{child}"
|
|
2091
|
+
end
|
|
2092
|
+
private :authenticated_ds_absence
|
|
2093
|
+
|
|
1160
2094
|
def get_nameservers_for(name, res = nil) # :nodoc:
|
|
1161
2095
|
# @TODO@ !!!
|
|
1162
2096
|
if (Dnssec.do_validation_with_recursor?)
|
|
@@ -1238,51 +2172,88 @@ module Dnsruby
|
|
|
1238
2172
|
end
|
|
1239
2173
|
|
|
1240
2174
|
def validate_no_rrsigs(msg) # :nodoc:
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
2175
|
+
with_validation { validate_no_rrsigs_internal(msg) }
|
|
2176
|
+
end
|
|
2177
|
+
|
|
2178
|
+
private def validate_no_rrsigs_internal(msg)
|
|
1244
2179
|
if (!msg.question()[0] && msg.answer.length == 0)
|
|
1245
|
-
# print "Returning Message insecure OK\n"
|
|
1246
2180
|
msg.security_level = Message::SecurityLevel.INSECURE
|
|
1247
2181
|
return true
|
|
1248
2182
|
end
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
return false
|
|
1260
|
-
end
|
|
2183
|
+
# An unsigned alias can lead into another zone. Insecurity of the
|
|
2184
|
+
# original QNAME says nothing about that zone's required records.
|
|
2185
|
+
operation = trust_operation
|
|
2186
|
+
operation[:insecure_names] ||= {}
|
|
2187
|
+
question = msg.question.first
|
|
2188
|
+
terminal, = terminal_answer(msg, question)
|
|
2189
|
+
names = [[question.qname, question.qclass], [terminal, question.qclass]]
|
|
2190
|
+
msg.section_rrsets.each do |section, rrsets|
|
|
2191
|
+
next if section == 'additional'
|
|
2192
|
+
rrsets.each { |rrset| names << [rrset.name, rrset.klass] unless rrset.rrs.empty? }
|
|
1261
2193
|
end
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
# print "Checking DLV for closest anchor\n"
|
|
1266
|
-
dlv_anchor = find_closest_dlv_anchor_for(qname)
|
|
1267
|
-
# print "Found DLV closest anchor :#{dlv_anchor}\n"
|
|
1268
|
-
if (dlv_anchor)
|
|
1269
|
-
actual_anchor = follow_chain(dlv_anchor, qname)
|
|
1270
|
-
# print "Actual anchor : #{actual_anchor}\n"
|
|
1271
|
-
if (actual_anchor)
|
|
1272
|
-
# print("DLV Anchor exists for #{qname}, but no signatures in #{msg}\n")
|
|
1273
|
-
TheLog.error("DLV Anchor exists for #{qname}, but no signatures in #{msg}")
|
|
1274
|
-
msg.security_level = Message::SecurityLevel.BOGUS
|
|
1275
|
-
return false
|
|
1276
|
-
end
|
|
1277
|
-
|
|
1278
|
-
end
|
|
2194
|
+
unless names.uniq.all? { |name, klass| discover_insecure_name(name, klass) }
|
|
2195
|
+
msg.security_level = Message::SecurityLevel.BOGUS
|
|
2196
|
+
return false
|
|
1279
2197
|
end
|
|
1280
|
-
# print "Returning Message insecure OK\n"
|
|
1281
2198
|
msg.security_level = Message::SecurityLevel.INSECURE
|
|
1282
2199
|
return true
|
|
1283
2200
|
end
|
|
1284
2201
|
|
|
1285
2202
|
def validate(msg, query)
|
|
2203
|
+
with_operational_validation(msg) { validate_internal(msg, query) }
|
|
2204
|
+
end
|
|
2205
|
+
|
|
2206
|
+
# Cached-key verification for Dnssec's validation fast path uses the same
|
|
2207
|
+
# algorithm policy and proof-lifetime accounting as chain discovery.
|
|
2208
|
+
def verify_for_validation(msg) # :nodoc:
|
|
2209
|
+
with_operational_validation(msg) { verify_result(msg) }
|
|
2210
|
+
end
|
|
2211
|
+
|
|
2212
|
+
def with_operational_validation(msg)
|
|
2213
|
+
with_validation do
|
|
2214
|
+
operation = trust_operation
|
|
2215
|
+
previous = operation.values_at(:operational, :insecure_rrsets, :insecure_names, :name_security,
|
|
2216
|
+
:insecurity_expirations, :last_insecure_expiration)
|
|
2217
|
+
operation[:operational] = true
|
|
2218
|
+
operation[:insecure_rrsets], operation[:insecure_names] = {}, {}
|
|
2219
|
+
operation[:name_security] = {}
|
|
2220
|
+
operation[:insecurity_expirations] = []
|
|
2221
|
+
begin
|
|
2222
|
+
result = yield
|
|
2223
|
+
if result == :insecure || (result == true && msg.security_level == Message::SecurityLevel.INSECURE)
|
|
2224
|
+
msg.header.ad = false
|
|
2225
|
+
bound_insecure_lifetime(msg, operation[:insecurity_expirations]) if result
|
|
2226
|
+
end
|
|
2227
|
+
result
|
|
2228
|
+
ensure
|
|
2229
|
+
operation[:operational], operation[:insecure_rrsets], operation[:insecure_names], operation[:name_security],
|
|
2230
|
+
operation[:insecurity_expirations], operation[:last_insecure_expiration] = previous
|
|
2231
|
+
end
|
|
2232
|
+
end
|
|
2233
|
+
end
|
|
2234
|
+
private :with_operational_validation
|
|
2235
|
+
|
|
2236
|
+
# An unsigned answer's TTL does not authenticate how long its insecure
|
|
2237
|
+
# classification is valid. Cache that classification only within the
|
|
2238
|
+
# authenticated no-DS/unsupported-DS proof and its complete trust chain.
|
|
2239
|
+
def bound_insecure_lifetime(msg, expirations)
|
|
2240
|
+
previous = msg.instance_variable_get(:@dnsruby_authenticated_expiration)
|
|
2241
|
+
if expirations.empty? || expirations.any?(&:nil?)
|
|
2242
|
+
msg.remove_instance_variable(:@dnsruby_authenticated_expiration) if previous
|
|
2243
|
+
return
|
|
2244
|
+
end
|
|
2245
|
+
signed_expirations = []
|
|
2246
|
+
msg.each_resource do |rr|
|
|
2247
|
+
deadline = rr.instance_variable_get(:@dnsruby_authenticated_expiration)
|
|
2248
|
+
signed_expirations << deadline if deadline
|
|
2249
|
+
end
|
|
2250
|
+
deadline = (expirations + signed_expirations + Array(previous)).min
|
|
2251
|
+
msg.instance_variable_set(:@dnsruby_authenticated_expiration, deadline)
|
|
2252
|
+
end
|
|
2253
|
+
private :bound_insecure_lifetime
|
|
2254
|
+
|
|
2255
|
+
private def validate_internal(msg, query)
|
|
2256
|
+
discard_unsupported_nsec3_proofs(msg)
|
|
1286
2257
|
if (msg.rrsets('RRSIG').length == 0)
|
|
1287
2258
|
return validate_no_rrsigs(msg)
|
|
1288
2259
|
end
|
|
@@ -1323,12 +2294,21 @@ module Dnsruby
|
|
|
1323
2294
|
if (msg.security_level.code != Message::SecurityLevel::SECURE)
|
|
1324
2295
|
begin
|
|
1325
2296
|
# print "Trying to verify one last time\n"
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
2297
|
+
result = begin
|
|
2298
|
+
# Cached target keys may already authenticate the entire answer.
|
|
2299
|
+
# A matching key tag alone cannot establish that during rollover.
|
|
2300
|
+
verify_result(msg)
|
|
2301
|
+
rescue VerifyError
|
|
2302
|
+
raise unless discover_signer_keys(msg)
|
|
2303
|
+
verify_result(msg)
|
|
2304
|
+
end
|
|
2305
|
+
if result
|
|
2306
|
+
set_verified_security_level(msg, result)
|
|
1329
2307
|
return true
|
|
1330
2308
|
end
|
|
1331
|
-
|
|
2309
|
+
error ||= VerifyError.new('Required DNSSEC records did not validate')
|
|
2310
|
+
rescue VerifyError => verification_error
|
|
2311
|
+
error ||= verification_error
|
|
1332
2312
|
end
|
|
1333
2313
|
end
|
|
1334
2314
|
if (error)
|
|
@@ -1344,7 +2324,110 @@ module Dnsruby
|
|
|
1344
2324
|
end
|
|
1345
2325
|
end
|
|
1346
2326
|
|
|
2327
|
+
# An alias can cross zone boundaries. Walking only the original QNAME
|
|
2328
|
+
# cannot discover keys for the target's answer or negative proof. These
|
|
2329
|
+
# lookups remain inside the caller's validation budget and trust operation.
|
|
2330
|
+
def discover_signer_keys(msg)
|
|
2331
|
+
discovered = {}
|
|
2332
|
+
attempted = false
|
|
2333
|
+
qname = msg.question.first&.qname
|
|
2334
|
+
msg.section_rrsets.each do |section, rrsets|
|
|
2335
|
+
next if section == 'additional'
|
|
2336
|
+
rrsets.each do |rrset|
|
|
2337
|
+
next if rrset.rrs.empty?
|
|
2338
|
+
if rrset.sigs.empty? || (trust_operation&.fetch(:operational, false) &&
|
|
2339
|
+
rrset.sigs.none? { |sig| algorithm_supported?(sig.algorithm, operational: true) })
|
|
2340
|
+
attempted = true if discover_insecure_rrset(rrset)
|
|
2341
|
+
next
|
|
2342
|
+
end
|
|
2343
|
+
begin
|
|
2344
|
+
signatures = valid_signatures(rrset)
|
|
2345
|
+
rescue VerifyError
|
|
2346
|
+
attempted = true if discover_insecure_rrset(rrset)
|
|
2347
|
+
next # Ordinary verification will report the invalid RRset.
|
|
2348
|
+
end
|
|
2349
|
+
signatures.map(&:signers_name).uniq.each do |zone|
|
|
2350
|
+
ValidationBudget.current&.check!(1)
|
|
2351
|
+
if qname && within_zone?(qname, zone) # The original chain already visited this zone.
|
|
2352
|
+
if msg.security_level == Message::SecurityLevel.INSECURE
|
|
2353
|
+
attempted = true if discover_insecure_rrset(rrset)
|
|
2354
|
+
end
|
|
2355
|
+
next
|
|
2356
|
+
end
|
|
2357
|
+
if discovered.key?(zone.canonical)
|
|
2358
|
+
attempted = true if discovered[zone.canonical] == :insecure && discover_insecure_rrset(rrset)
|
|
2359
|
+
next
|
|
2360
|
+
end
|
|
2361
|
+
discovered[zone.canonical] = :failed
|
|
2362
|
+
anchor = find_closest_anchor_for(zone)
|
|
2363
|
+
anchor ||= find_closest_dlv_anchor_for(zone) if @verifier_type == VerifierType::DLV && @added_dlv_key
|
|
2364
|
+
unless anchor
|
|
2365
|
+
discovered[zone.canonical] = :insecure
|
|
2366
|
+
attempted = true if discover_insecure_rrset(rrset)
|
|
2367
|
+
next
|
|
2368
|
+
end
|
|
2369
|
+
attempted = true
|
|
2370
|
+
begin
|
|
2371
|
+
if follow_chain(anchor, zone)
|
|
2372
|
+
discovered[zone.canonical] = :secure
|
|
2373
|
+
else
|
|
2374
|
+
discovered[zone.canonical] = :insecure
|
|
2375
|
+
# Honor a more-specific configured anchor at the owner, even
|
|
2376
|
+
# when the signature names an unsigned ancestor zone.
|
|
2377
|
+
discover_insecure_rrset(rrset)
|
|
2378
|
+
end
|
|
2379
|
+
rescue VerifyError
|
|
2380
|
+
# An alternative signature may still validate this RRset. A
|
|
2381
|
+
# budget abort deliberately propagates past this fallback.
|
|
2382
|
+
end
|
|
2383
|
+
end
|
|
2384
|
+
end
|
|
2385
|
+
end
|
|
2386
|
+
if trust_operation&.fetch(:insecure_names, {})&.any? && (question = msg.question.first)
|
|
2387
|
+
terminal, answered = terminal_answer(msg, question)
|
|
2388
|
+
discover_insecure_name(terminal, question.qclass) if !answered && terminal != question.qname
|
|
2389
|
+
end
|
|
2390
|
+
attempted
|
|
2391
|
+
end
|
|
2392
|
+
private :discover_signer_keys
|
|
2393
|
+
|
|
2394
|
+
private def discover_insecure_rrset(rrset)
|
|
2395
|
+
return false unless discover_insecure_name(rrset.name, rrset.klass)
|
|
2396
|
+
trust_operation[:insecure_rrsets][[rrset.name.canonical, rrset.klass.code, rrset.type.code]] = true
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
def discover_insecure_name(name, klass)
|
|
2400
|
+
operation = trust_operation
|
|
2401
|
+
return false unless operation && operation[:insecure_names]
|
|
2402
|
+
identity = [name.canonical, klass.code]
|
|
2403
|
+
return true if operation[:insecure_names][identity]
|
|
2404
|
+
classifications = operation[:name_security] ||= {}
|
|
2405
|
+
return classifications[identity] if classifications.key?(identity)
|
|
2406
|
+
ValidationBudget.current&.check!(1)
|
|
2407
|
+
anchors = [find_closest_anchor_for(name)]
|
|
2408
|
+
anchors << find_closest_dlv_anchor_for(name) if @verifier_type == VerifierType::DLV && @added_dlv_key
|
|
2409
|
+
anchors = anchors.select { |anchor| anchor && anchor.klass == klass }.uniq
|
|
2410
|
+
# No applicable anchor is an unanchored result for this verifier. It
|
|
2411
|
+
# must not turn into a reusable authenticated proof of insecurity.
|
|
2412
|
+
operation[:insecurity_expirations]&.push(nil) if anchors.empty?
|
|
2413
|
+
if anchors.any? { |anchor| follow_chain(anchor, name) }
|
|
2414
|
+
classifications[identity] = false
|
|
2415
|
+
return false
|
|
2416
|
+
end
|
|
2417
|
+
operation[:insecure_names][identity] = true
|
|
2418
|
+
classifications[identity] = true
|
|
2419
|
+
rescue ValidationLimitError
|
|
2420
|
+
raise
|
|
2421
|
+
rescue VerifyError
|
|
2422
|
+
false # Missing or invalid delegation evidence remains a validation failure.
|
|
2423
|
+
end
|
|
2424
|
+
private :discover_insecure_name
|
|
2425
|
+
|
|
1347
2426
|
def try_to_follow_from_anchor(closest_anchor, msg, qname) # :nodoc:
|
|
2427
|
+
with_validation { try_to_follow_from_anchor_internal(closest_anchor, msg, qname) }
|
|
2428
|
+
end
|
|
2429
|
+
|
|
2430
|
+
private def try_to_follow_from_anchor_internal(closest_anchor, msg, qname)
|
|
1348
2431
|
error = nil
|
|
1349
2432
|
if (closest_anchor)
|
|
1350
2433
|
# Then try to descend to the level we're interested in
|
|
@@ -1356,7 +2439,7 @@ module Dnsruby
|
|
|
1356
2439
|
msg.security_level = Message::SecurityLevel.BOGUS
|
|
1357
2440
|
return e
|
|
1358
2441
|
end
|
|
1359
|
-
#
|
|
2442
|
+
# follow_chain returns false only for an authenticated unsigned delegation.
|
|
1360
2443
|
if (!actual_anchor)
|
|
1361
2444
|
TheLog.debug("Unable to follow chain from anchor : #{closest_anchor.name}")
|
|
1362
2445
|
msg.security_level = Message::SecurityLevel.INSECURE
|
|
@@ -1366,9 +2449,9 @@ module Dnsruby
|
|
|
1366
2449
|
TheLog.debug("Found anchor #{actual_anchor.name}, #{actual_anchor.type} for #{qname} : #{actual_anchor_keys}")
|
|
1367
2450
|
# print "Found anchor #{actual_anchor.name}, #{actual_anchor.type} for #{qname} : #{actual_anchor_keys}\n"
|
|
1368
2451
|
begin
|
|
1369
|
-
if (
|
|
2452
|
+
if (result = verify_result(msg, actual_anchor))
|
|
1370
2453
|
TheLog.debug("Validated #{qname}")
|
|
1371
|
-
msg
|
|
2454
|
+
set_verified_security_level(msg, result)
|
|
1372
2455
|
end
|
|
1373
2456
|
rescue VerifyError => e
|
|
1374
2457
|
TheLog.info("BOGUS #{qname}! Error : #{e}")
|
|
@@ -1384,5 +2467,12 @@ module Dnsruby
|
|
|
1384
2467
|
return error
|
|
1385
2468
|
end
|
|
1386
2469
|
|
|
2470
|
+
def set_verified_security_level(msg, result)
|
|
2471
|
+
msg.security_level = result == :secure ? Message::SecurityLevel.SECURE : Message::SecurityLevel.INSECURE
|
|
2472
|
+
msg.header.ad = false if result == :insecure
|
|
2473
|
+
msg.security_error = nil
|
|
2474
|
+
end
|
|
2475
|
+
private :set_verified_security_level
|
|
2476
|
+
|
|
1387
2477
|
end
|
|
1388
|
-
end
|
|
2478
|
+
end
|