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
|
@@ -21,18 +21,10 @@ rescue LoadError
|
|
|
21
21
|
end
|
|
22
22
|
module Dnsruby
|
|
23
23
|
class RR
|
|
24
|
-
# TSIG
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# dynamic updates as coming from an approved client, or to authenticate
|
|
29
|
-
# responses as coming from an approved recursive name server."
|
|
30
|
-
#
|
|
31
|
-
# A Dnsruby::RR::TSIG can represent the data present in a TSIG RR.
|
|
32
|
-
# However, it can also represent the data (specified in RFC2845) used
|
|
33
|
-
# to sign or verify a DNS message.
|
|
34
|
-
#
|
|
35
|
-
#
|
|
24
|
+
# A TSIG record contains authentication data for a DNS message. This class
|
|
25
|
+
# also holds the shared key and provides signing and verification helpers.
|
|
26
|
+
# RFC 8945 section 4 describes the record format and MAC calculation.
|
|
27
|
+
#
|
|
36
28
|
# Example code :
|
|
37
29
|
# res = Dnsruby::Resolver.new("ns0.validation-test-servers.nominet.org.uk")
|
|
38
30
|
#
|
|
@@ -58,11 +50,41 @@ module Dnsruby
|
|
|
58
50
|
|
|
59
51
|
DEFAULT_FUDGE = 300
|
|
60
52
|
|
|
61
|
-
DEFAULT_ALGORITHM =
|
|
53
|
+
DEFAULT_ALGORITHM = HMAC_SHA256
|
|
54
|
+
|
|
55
|
+
DIGESTS = {
|
|
56
|
+
'hmac-md5.sig-alg.reg.int' => 'MD5', 'hmac-sha1' => 'SHA1',
|
|
57
|
+
'hmac-sha256' => 'SHA256', 'hmac-sha512' => 'SHA512'
|
|
58
|
+
}.freeze
|
|
59
|
+
private_constant :DIGESTS
|
|
60
|
+
LEGACY_WARNING_MUTEX = Mutex.new
|
|
61
|
+
private_constant :LEGACY_WARNING_MUTEX
|
|
62
|
+
|
|
63
|
+
# BADTIME and BADTRUNC replies carry a MAC; BADKEY and BADSIG do not
|
|
64
|
+
# (RFC 8945 sections 5.3.2 and 5.4). Only authenticated errors can end
|
|
65
|
+
# a pending query.
|
|
66
|
+
SIGNED_ERRORS = {18 => 'BADTIME', 22 => 'BADTRUNC'}.freeze
|
|
67
|
+
private_constant :SIGNED_ERRORS
|
|
68
|
+
|
|
69
|
+
# Verification removes the TSIG RR from the Message. Keep its saved
|
|
70
|
+
# record long enough to report authenticated errors consistently for
|
|
71
|
+
# ordinary queries and zone transfers (RFC 8945 sections 5.4.3/5.4.4).
|
|
72
|
+
def self.authenticated_error(response, signature) #:nodoc:
|
|
73
|
+
return unless signature && response.verified? && response.header.get_header_rcode == RCode.NOTAUTH
|
|
74
|
+
description = SIGNED_ERRORS[response.tsigerror]
|
|
75
|
+
return unless description
|
|
76
|
+
diagnostic = "Authenticated TSIG #{description} (#{response.tsigerror})"
|
|
77
|
+
if response.tsigerror == RCode.BADTIME
|
|
78
|
+
high, low = signature.other_data.unpack('nN')
|
|
79
|
+
diagnostic += "; server time #{(high << 32) | low} seconds since Unix epoch"
|
|
80
|
+
end
|
|
81
|
+
Dnsruby.log.error { diagnostic }
|
|
82
|
+
TsigError.new(diagnostic)
|
|
83
|
+
end
|
|
62
84
|
|
|
63
85
|
# Generates a TSIG record and adds it to the message.
|
|
64
86
|
# Takes an optional original_request argument for the case where this is
|
|
65
|
-
# a response to a query (
|
|
87
|
+
# a response to a query (RFC 8945 section 4.3.1).
|
|
66
88
|
#
|
|
67
89
|
# Message#tsigstate will be set to :Signed.
|
|
68
90
|
def apply(message, original_request=nil)
|
|
@@ -80,8 +102,27 @@ module Dnsruby
|
|
|
80
102
|
end
|
|
81
103
|
|
|
82
104
|
|
|
83
|
-
# Generates a TSIG record
|
|
105
|
+
# Generates a TSIG record. The caller must authenticate original_request
|
|
106
|
+
# before using it to sign a response.
|
|
84
107
|
def generate(msg, original_request = nil, data="", msg_bytes=nil, tsig_rr=self)#:nodoc: all
|
|
108
|
+
verifying = !msg_bytes.nil?
|
|
109
|
+
unless msg_bytes
|
|
110
|
+
tsig_rr.warn_if_legacy_algorithm
|
|
111
|
+
if [msg.answer, msg.authority, msg.additional].any? { |section| section.any? { |rr| rr.type == Types::TSIG } }
|
|
112
|
+
raise EncodeError, 'Message already contains a TSIG'
|
|
113
|
+
end
|
|
114
|
+
if !msg.header.qr && tsig_rr.error != RCode.NOERROR
|
|
115
|
+
raise EncodeError, 'A TSIG request must have error NOERROR'
|
|
116
|
+
end
|
|
117
|
+
if msg.header.qr && [RCode.BADSIG, RCode.BADKEY].include?(tsig_rr.error)
|
|
118
|
+
raise EncodeError, 'BADKEY and BADSIG responses must be unsigned'
|
|
119
|
+
end
|
|
120
|
+
if original_request && tsig_rr.error == RCode.BADTIME
|
|
121
|
+
tsig_rr = tsig_rr.dup
|
|
122
|
+
tsig_rr.time_signed = original_request.tsig.time_signed
|
|
123
|
+
tsig_rr.fudge = original_request.tsig.fudge
|
|
124
|
+
end
|
|
125
|
+
end
|
|
85
126
|
time_signed=@time_signed
|
|
86
127
|
if (!time_signed)
|
|
87
128
|
time_signed=Time.now.to_i
|
|
@@ -89,84 +130,95 @@ module Dnsruby
|
|
|
89
130
|
if (tsig_rr.time_signed)
|
|
90
131
|
time_signed = tsig_rr.time_signed
|
|
91
132
|
end
|
|
133
|
+
validate_wire_fields(tsig_rr, time_signed) unless msg_bytes
|
|
134
|
+
if !msg_bytes && msg.header.qr && tsig_rr.other_size != (tsig_rr.error == RCode.BADTIME ? 6 : 0)
|
|
135
|
+
raise EncodeError, 'Invalid TSIG response Other Data length'
|
|
136
|
+
end
|
|
137
|
+
original_id = verifying ? tsig_rr.original_id : (original_request ? original_request.tsig.original_id : msg.header.id)
|
|
92
138
|
|
|
93
139
|
if (original_request)
|
|
94
|
-
#
|
|
95
|
-
# hmac.update(pack("H*", request_mac))
|
|
140
|
+
# Responses include the request MAC exactly as transmitted.
|
|
96
141
|
mac_bytes = MessageEncoder.new {|m|
|
|
97
142
|
m.put_pack('n', original_request.tsig.mac_size)
|
|
98
143
|
m.put_bytes(original_request.tsig.mac)
|
|
99
144
|
}.to_s
|
|
100
145
|
data += mac_bytes
|
|
101
|
-
|
|
102
|
-
if (tsig_rr != self)
|
|
103
|
-
msg.header.id = tsig_rr.original_id
|
|
104
|
-
else
|
|
105
|
-
msg.header.id = original_request.header.id
|
|
106
|
-
end
|
|
146
|
+
msg.header.id = original_request.header.id unless msg_bytes
|
|
107
147
|
end
|
|
108
148
|
|
|
109
149
|
if (!msg_bytes)
|
|
110
150
|
msg_bytes = msg.encode
|
|
111
|
-
data += msg_bytes
|
|
151
|
+
data += [original_id].pack('n') + msg_bytes.byteslice(2..-1)
|
|
112
152
|
else
|
|
113
|
-
|
|
114
|
-
# It is the last record, so we can strip it if we know where it starts
|
|
115
|
-
# We must also poke the header ARcount to decrement it
|
|
116
|
-
msg_bytes = Header.decrement_arcount_encoded(msg_bytes)
|
|
117
|
-
data += msg_bytes[0, msg.tsigstart]
|
|
153
|
+
data += verification_message(msg, msg_bytes, tsig_rr)
|
|
118
154
|
end
|
|
119
155
|
|
|
120
156
|
data += sig_data(tsig_rr, time_signed)
|
|
121
157
|
|
|
122
158
|
mac = calculate_mac(tsig_rr.algorithm, data)
|
|
159
|
+
if !verifying && @mac_length
|
|
160
|
+
minimum = [10, mac.bytesize / 2].max
|
|
161
|
+
unless (minimum..mac.bytesize).cover?(@mac_length)
|
|
162
|
+
raise EncodeError, "TSIG MAC length must be between #{minimum} and #{mac.bytesize} octets"
|
|
163
|
+
end
|
|
164
|
+
if original_request && @mac_length < original_request.tsig.mac_size
|
|
165
|
+
raise EncodeError, 'TSIG response MAC must not be shorter than the request MAC'
|
|
166
|
+
end
|
|
167
|
+
mac = mac.byteslice(0, @mac_length)
|
|
168
|
+
end
|
|
123
169
|
|
|
124
170
|
mac_size = mac.length
|
|
125
171
|
|
|
126
|
-
new_tsig_rr = Dnsruby::RR.create(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
:fudge => tsig_rr.fudge,
|
|
133
|
-
:key => @key,
|
|
134
|
-
:mac => mac,
|
|
135
|
-
:mac_size => mac_size,
|
|
136
|
-
:error => tsig_rr.error,
|
|
137
|
-
:time_signed => time_signed,
|
|
138
|
-
:original_id => msg.header.id
|
|
139
|
-
})
|
|
172
|
+
new_tsig_rr = Dnsruby::RR.create(name: tsig_rr.name, type: Types.TSIG,
|
|
173
|
+
ttl: tsig_rr.ttl, klass: tsig_rr.klass, key: @key, mac_length: @mac_length)
|
|
174
|
+
# Copy fields directly so verifying an incoming record doesn't trigger
|
|
175
|
+
# the warning for configuring a legacy key.
|
|
176
|
+
new_tsig_rr.from_data([tsig_rr.algorithm, time_signed, tsig_rr.fudge,
|
|
177
|
+
mac_size, mac, original_id, tsig_rr.error, tsig_rr.other_size, tsig_rr.other_data])
|
|
140
178
|
return new_tsig_rr
|
|
141
179
|
|
|
142
180
|
end
|
|
143
181
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (algorithm.to_s.downcase == HMAC_MD5.to_s.downcase)
|
|
156
|
-
mac = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, data)
|
|
157
|
-
elsif (algorithm == HMAC_SHA1)
|
|
158
|
-
mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, data)
|
|
159
|
-
elsif (algorithm == HMAC_SHA256)
|
|
160
|
-
mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, data)
|
|
161
|
-
elsif (algorithm == HMAC_SHA512)
|
|
162
|
-
mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA512.new, key, data)
|
|
163
|
-
else
|
|
164
|
-
# Should we allow client to pass in their own signing function?
|
|
165
|
-
raise VerifyError.new("Algorithm #{algorithm} unsupported by TSIG")
|
|
182
|
+
def warn_if_legacy_algorithm #:nodoc: all
|
|
183
|
+
name = @algorithm.to_s.downcase.sub(/\.$/, '')
|
|
184
|
+
return unless [HMAC_MD5.to_s.downcase.sub(/\.$/, ''), 'hmac-md5'].include?(name)
|
|
185
|
+
warn = LEGACY_WARNING_MUTEX.synchronize do
|
|
186
|
+
unless TSIG.instance_variable_get(:@legacy_warning_issued)
|
|
187
|
+
TSIG.instance_variable_set(:@legacy_warning_issued, true)
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
if warn
|
|
192
|
+
Dnsruby.log.warn('HMAC-MD5 TSIG is enabled as a legacy compatibility exception. RFC 8945 says it MUST NOT be used. Update the TSIG key and configure hmac-sha256 on both the DNS server and client.')
|
|
166
193
|
end
|
|
167
|
-
return mac
|
|
168
194
|
end
|
|
169
195
|
|
|
196
|
+
def calculate_mac(algorithm, data)
|
|
197
|
+
name = algorithm.to_s.downcase.sub(/\.$/, '')
|
|
198
|
+
digest = DIGESTS[name]
|
|
199
|
+
raise VerifyError, "Algorithm #{algorithm} unsupported by TSIG" unless digest
|
|
200
|
+
OpenSSL::HMAC.digest(digest, @key.gsub(' ', '').unpack1('m*'), data)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def verification_message(message, bytes, signature)
|
|
204
|
+
wire = Header.decrement_arcount_encoded(bytes.dup).byteslice(0, message.tsigstart)
|
|
205
|
+
wire[0, 2] = [signature.original_id].pack('n')
|
|
206
|
+
wire
|
|
207
|
+
end
|
|
208
|
+
private :verification_message
|
|
209
|
+
|
|
210
|
+
def validate_wire_fields(signature, time_signed)
|
|
211
|
+
valid = signature.klass == Classes.ANY && signature.ttl == 0 &&
|
|
212
|
+
time_signed.is_a?(Integer) && (0...(1 << 48)).cover?(time_signed) &&
|
|
213
|
+
[signature.fudge, signature.original_id, signature.error, signature.mac_size, signature.other_size].all? do |value|
|
|
214
|
+
value.is_a?(Integer) && (0..65_535).cover?(value)
|
|
215
|
+
end
|
|
216
|
+
valid &&= signature.mac.is_a?(String) && signature.mac_size == signature.mac.bytesize &&
|
|
217
|
+
signature.other_data.is_a?(String) && signature.other_size == signature.other_data.bytesize
|
|
218
|
+
raise EncodeError, 'Invalid TSIG fields' unless valid
|
|
219
|
+
end
|
|
220
|
+
private :validate_wire_fields
|
|
221
|
+
|
|
170
222
|
# Private method to return the TSIG RR data to be signed
|
|
171
223
|
def sig_data(tsig_rr, time_signed=@time_signed) #:nodoc: all
|
|
172
224
|
return MessageEncoder.new { |msg|
|
|
@@ -184,135 +236,106 @@ module Dnsruby
|
|
|
184
236
|
}.to_s
|
|
185
237
|
end
|
|
186
238
|
|
|
187
|
-
# Verify a response
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# Message#tsigstate will be set to one of :
|
|
192
|
-
# * :Failed
|
|
193
|
-
# * :Verified
|
|
239
|
+
# Verify a response using the original query's MAC (RFC 8945 section 5.4).
|
|
240
|
+
# Removes the TSIG record and sets Message#tsigstate and Message#tsigerror.
|
|
241
|
+
# Returns false for server errors, even with a valid MAC. Check the
|
|
242
|
+
# Verified state and error code to identify authenticated errors.
|
|
194
243
|
def verify(query, response, response_bytes, buf="")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# acceptable response; the client SHOULD log an error and continue to
|
|
208
|
-
# wait for a signed response until the request times out.
|
|
209
|
-
|
|
210
|
-
# So, this verify method should simply remove the TSIG RR and calculate
|
|
211
|
-
# the MAC (using original request MAC if required).
|
|
212
|
-
# Should set tsigstate on packet appropriately, and return error.
|
|
213
|
-
# Side effect is packet is stripped of TSIG.
|
|
214
|
-
# Resolver (or client) can then decide what to do...
|
|
215
|
-
|
|
216
|
-
msg_tsig_rr = response.tsig
|
|
217
|
-
if (!verify_common(response))
|
|
218
|
-
return false
|
|
244
|
+
signature = response.tsig
|
|
245
|
+
signed_error = signature && response.header.get_header_rcode == RCode.NOTAUTH &&
|
|
246
|
+
SIGNED_ERRORS.key?(signature.error)
|
|
247
|
+
return false unless verify_common_fields(response, signed_error)
|
|
248
|
+
|
|
249
|
+
expected = generate(response, query, buf, response_bytes, signature).mac
|
|
250
|
+
return false unless verify_mac(response, signature, expected)
|
|
251
|
+
# A BADTIME reply echoes the request's timers; only Other Data carries
|
|
252
|
+
# the server's clock (RFC 8945 section 5.2.3).
|
|
253
|
+
if signed_error && signature.error == RCode.BADTIME && query && query.tsig &&
|
|
254
|
+
(signature.time_signed != query.tsig.time_signed || signature.fudge != query.tsig.fudge)
|
|
255
|
+
return verification_failed(response, RCode.BADTIME)
|
|
219
256
|
end
|
|
257
|
+
return false unless verify_time(response, signature)
|
|
258
|
+
return false unless verify_truncation_policy(response, signature)
|
|
259
|
+
response.tsigstate = :Verified
|
|
260
|
+
response.tsigerror = signature.error
|
|
261
|
+
signature.error == RCode.NOERROR
|
|
262
|
+
end
|
|
220
263
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (msg_tsig_rr.mac == new_msg_tsig_rr.mac)
|
|
224
|
-
response.tsigstate = :Verified
|
|
225
|
-
response.tsigerror = RCode.NOERROR
|
|
226
|
-
return true
|
|
227
|
-
else
|
|
228
|
-
response.tsigstate = :Failed
|
|
229
|
-
response.tsigerror = RCode.BADSIG
|
|
230
|
-
return false
|
|
231
|
-
end
|
|
264
|
+
def verify_common(response) #:nodoc: all
|
|
265
|
+
verify_common_fields(response, false)
|
|
232
266
|
end
|
|
233
267
|
|
|
234
|
-
def
|
|
235
|
-
|
|
268
|
+
def verification_failed(response, error)
|
|
269
|
+
response.tsigstate = :Failed
|
|
270
|
+
response.tsigerror = error
|
|
271
|
+
false
|
|
272
|
+
end
|
|
236
273
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
274
|
+
def verify_common_fields(response, signed_error)
|
|
275
|
+
signature = response.tsig
|
|
276
|
+
return verification_failed(response, RCode.FORMERR) unless signature
|
|
277
|
+
response.additional.delete(signature)
|
|
278
|
+
response.header.arcount -= 1
|
|
279
|
+
return verification_failed(response, RCode.FORMERR) unless signature.is_a?(TSIG)
|
|
280
|
+
begin
|
|
281
|
+
validate_wire_fields(signature, signature.time_signed)
|
|
282
|
+
rescue EncodeError
|
|
283
|
+
return verification_failed(response, RCode.FORMERR)
|
|
241
284
|
end
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return
|
|
285
|
+
if response.header.qr && signature.other_size != (signature.error == RCode.BADTIME ? 6 : 0)
|
|
286
|
+
return verification_failed(response, RCode.FORMERR)
|
|
287
|
+
end
|
|
288
|
+
unless signature.name.downcase == @name.downcase && signature.algorithm.downcase == @algorithm.downcase &&
|
|
289
|
+
DIGESTS.key?(signature.algorithm.to_s.downcase.sub(/\.$/, ''))
|
|
290
|
+
return verification_failed(response, RCode.BADKEY)
|
|
291
|
+
end
|
|
292
|
+
if signature.error != RCode.NOERROR && !signed_error
|
|
293
|
+
return verification_failed(response, signature.error)
|
|
251
294
|
end
|
|
295
|
+
true
|
|
296
|
+
end
|
|
252
297
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
response
|
|
257
|
-
return false
|
|
298
|
+
def verify_mac(response, signature, expected)
|
|
299
|
+
minimum = [10, expected.bytesize / 2].max
|
|
300
|
+
unless (minimum..expected.bytesize).cover?(signature.mac_size)
|
|
301
|
+
return verification_failed(response, RCode.FORMERR)
|
|
258
302
|
end
|
|
303
|
+
unless OpenSSL.fixed_length_secure_compare(expected.byteslice(0, signature.mac_size), signature.mac)
|
|
304
|
+
return verification_failed(response, RCode.BADSIG)
|
|
305
|
+
end
|
|
306
|
+
true
|
|
307
|
+
end
|
|
259
308
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
Dnsruby.log.error("TSIG failed with BADTIME")
|
|
264
|
-
response.tsigstate = :Failed
|
|
265
|
-
response.tsigerror = RCode.BADTIME
|
|
266
|
-
return false
|
|
309
|
+
def verify_time(response, signature)
|
|
310
|
+
unless (Time.now.to_i - signature.time_signed).abs <= signature.fudge
|
|
311
|
+
return verification_failed(response, RCode.BADTIME)
|
|
267
312
|
end
|
|
313
|
+
true
|
|
314
|
+
end
|
|
268
315
|
|
|
269
|
-
|
|
316
|
+
def verify_truncation_policy(response, signature)
|
|
317
|
+
# RFC 8945 section 7 permits a local minimum above the protocol limit.
|
|
318
|
+
if @mac_length && signature.mac_size < @mac_length
|
|
319
|
+
return verification_failed(response, 22) # BADTRUNC
|
|
320
|
+
end
|
|
321
|
+
true
|
|
270
322
|
end
|
|
323
|
+
private :verification_failed, :verify_common_fields, :verify_mac, :verify_time, :verify_truncation_policy
|
|
271
324
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
325
|
+
def initialize_copy(original)
|
|
326
|
+
super
|
|
327
|
+
@buf = @buf.dup if @buf
|
|
328
|
+
end
|
|
329
|
+
private :initialize_copy
|
|
330
|
+
|
|
331
|
+
# Verify a stream, such as a zone transfer. Each signature covers the
|
|
332
|
+
# messages since the previous one. RFC 8945 section 5.3.1 requires senders
|
|
333
|
+
# to sign every message. For compatibility, receivers must accept up to
|
|
334
|
+
# 99 unsigned messages between signatures. The caller must also check
|
|
335
|
+
# that the final message is signed.
|
|
336
|
+
#
|
|
337
|
+
# Sets response.tsigstate to :Verified, :Intermediate or :Failed.
|
|
282
338
|
def verify_envelope(response, response_bytes)
|
|
283
|
-
# RFC2845 Section 4.4
|
|
284
|
-
# -----
|
|
285
|
-
# A DNS TCP session can include multiple DNS envelopes. This is, for
|
|
286
|
-
# example, commonly used by zone transfer. Using TSIG on such a
|
|
287
|
-
# connection can protect the connection from hijacking and provide data
|
|
288
|
-
# integrity. The TSIG MUST be included on the first and last DNS
|
|
289
|
-
# envelopes. It can be optionally placed on any intermediary
|
|
290
|
-
# envelopes. It is expensive to include it on every envelopes, but it
|
|
291
|
-
# MUST be placed on at least every 100'th envelope. The first envelope
|
|
292
|
-
# is processed as a standard answer, and subsequent messages have the
|
|
293
|
-
# following digest components:
|
|
294
|
-
#
|
|
295
|
-
# * Prior Digest (running)
|
|
296
|
-
# * DNS Messages (any unsigned messages since the last TSIG)
|
|
297
|
-
# * TSIG Timers (current message)
|
|
298
|
-
#
|
|
299
|
-
# This allows the client to rapidly detect when the session has been
|
|
300
|
-
# altered; at which point it can close the connection and retry. If a
|
|
301
|
-
# client TSIG verification fails, the client MUST close the connection.
|
|
302
|
-
# If the client does not receive TSIG records frequently enough (as
|
|
303
|
-
# specified above) it SHOULD assume the connection has been hijacked
|
|
304
|
-
# and it SHOULD close the connection. The client SHOULD treat this the
|
|
305
|
-
# same way as they would any other interrupted transfer (although the
|
|
306
|
-
# exact behavior is not specified).
|
|
307
|
-
# -----
|
|
308
|
-
#
|
|
309
|
-
# Each time a new envelope comes in, this method is called on the QUERY TSIG RR.
|
|
310
|
-
# It will set the response tsigstate to :Verified :Intermediate or :Failed
|
|
311
|
-
# as appropriate.
|
|
312
|
-
|
|
313
|
-
# Keep digest going of messages as they come in (and mark them intermediate)
|
|
314
|
-
# When TSIG comes in, work out what key should be and check. If OK, mark
|
|
315
|
-
# verified. Can reset digest then.
|
|
316
339
|
if (!@buf)
|
|
317
340
|
@num_envelopes = 0
|
|
318
341
|
@last_signed = 0
|
|
@@ -323,16 +346,15 @@ module Dnsruby
|
|
|
323
346
|
Dnsruby.log.debug("Receiving intermediate envelope in TSIG TCP session")
|
|
324
347
|
response.tsigstate = :Intermediate
|
|
325
348
|
response.tsigerror = RCode.NOERROR
|
|
326
|
-
@buf
|
|
327
|
-
return
|
|
349
|
+
@buf << response_bytes
|
|
350
|
+
return true
|
|
328
351
|
else
|
|
352
|
+
response.tsigerror = RCode.FORMERR
|
|
329
353
|
response.tsigstate = :Failed
|
|
330
354
|
Dnsruby.log.error("Expecting signed packet")
|
|
331
355
|
return false
|
|
332
356
|
end
|
|
333
357
|
end
|
|
334
|
-
@last_signed = @num_envelopes
|
|
335
|
-
|
|
336
358
|
# We have a TSIG - process it!
|
|
337
359
|
tsig = response.tsig
|
|
338
360
|
if (@num_envelopes == 1)
|
|
@@ -345,6 +367,8 @@ module Dnsruby
|
|
|
345
367
|
m.put_bytes(tsig.mac)
|
|
346
368
|
}.to_s
|
|
347
369
|
@buf = mac_bytes
|
|
370
|
+
@last_signed = @num_envelopes
|
|
371
|
+
@last_time_signed = tsig.time_signed
|
|
348
372
|
end
|
|
349
373
|
return ok
|
|
350
374
|
end
|
|
@@ -354,9 +378,7 @@ module Dnsruby
|
|
|
354
378
|
return false
|
|
355
379
|
end
|
|
356
380
|
|
|
357
|
-
|
|
358
|
-
response_bytes = Header.decrement_arcount_encoded(response_bytes)
|
|
359
|
-
@buf += response_bytes[0, response.tsigstart]
|
|
381
|
+
data = @buf + verification_message(response, response_bytes, tsig)
|
|
360
382
|
|
|
361
383
|
# Let's add the timers
|
|
362
384
|
timers_data = MessageEncoder.new { |msg|
|
|
@@ -365,20 +387,21 @@ module Dnsruby
|
|
|
365
387
|
msg.put_pack('nN', time_high, time_low)
|
|
366
388
|
msg.put_pack('n', tsig.fudge)
|
|
367
389
|
}.to_s
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if
|
|
373
|
-
|
|
374
|
-
response.tsigstate = :Failed
|
|
375
|
-
return false
|
|
390
|
+
data += timers_data
|
|
391
|
+
mac = calculate_mac(tsig.algorithm, data)
|
|
392
|
+
return false unless verify_mac(response, tsig, mac)
|
|
393
|
+
return false unless verify_time(response, tsig)
|
|
394
|
+
if @last_time_signed && tsig.time_signed < @last_time_signed
|
|
395
|
+
return verification_failed(response, RCode.BADTIME)
|
|
376
396
|
end
|
|
397
|
+
return false unless verify_truncation_policy(response, tsig)
|
|
377
398
|
mac_bytes = MessageEncoder.new {|m|
|
|
378
|
-
m.put_pack('n',
|
|
379
|
-
m.put_bytes(mac)
|
|
399
|
+
m.put_pack('n', tsig.mac_size)
|
|
400
|
+
m.put_bytes(tsig.mac)
|
|
380
401
|
}.to_s
|
|
381
402
|
@buf=mac_bytes
|
|
403
|
+
@last_signed = @num_envelopes
|
|
404
|
+
@last_time_signed = tsig.time_signed
|
|
382
405
|
|
|
383
406
|
response.tsigstate = :Verified
|
|
384
407
|
response.tsigerror = RCode.NOERROR
|
|
@@ -391,7 +414,9 @@ module Dnsruby
|
|
|
391
414
|
ClassHash[[TypeValue, Classes::ANY]] = self #:nodoc: all
|
|
392
415
|
|
|
393
416
|
# Gets or sets the domain name that specifies the name of the algorithm.
|
|
394
|
-
#
|
|
417
|
+
# Supported algorithms are hmac-md5, hmac-sha1, hmac-sha256 and hmac-sha512.
|
|
418
|
+
# SHA-256 is the default. You can select MD5 for legacy servers, but its
|
|
419
|
+
# use does not comply with RFC 8945 section 6.
|
|
395
420
|
#
|
|
396
421
|
# rr.algorithm=(algorithm_name)
|
|
397
422
|
# print "algorithm = ", rr.algorithm, "\n"
|
|
@@ -419,16 +444,26 @@ module Dnsruby
|
|
|
419
444
|
attr_reader :fudge
|
|
420
445
|
|
|
421
446
|
# Returns the number of octets in the message authentication code (MAC).
|
|
422
|
-
#
|
|
423
|
-
# before this will return anything meaningful.
|
|
447
|
+
# Set when a record is decoded or a message is signed.
|
|
424
448
|
#
|
|
425
449
|
# print "MAC size = ", rr.mac_size, "\n"
|
|
426
450
|
#
|
|
427
451
|
attr_accessor :mac_size
|
|
428
452
|
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
453
|
+
# MAC length in bytes for signing, and minimum length to accept.
|
|
454
|
+
# nil signs with the full digest and accepts the RFC 8945 minimum.
|
|
455
|
+
# mac_size holds the actual length in a signed or decoded record.
|
|
456
|
+
attr_reader :mac_length
|
|
457
|
+
|
|
458
|
+
def mac_length=(length)
|
|
459
|
+
unless length.nil? || (length.is_a?(Integer) && length.positive?)
|
|
460
|
+
raise ArgumentError, 'TSIG mac_length must be a positive integer or nil'
|
|
461
|
+
end
|
|
462
|
+
@mac_length = length
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Returns the message authentication code (MAC) as a binary string.
|
|
466
|
+
# Set when a record is decoded or a message is signed.
|
|
432
467
|
#
|
|
433
468
|
# print "MAC = ", rr.mac, "\n"
|
|
434
469
|
#
|
|
@@ -442,7 +477,7 @@ module Dnsruby
|
|
|
442
477
|
attr_accessor :original_id
|
|
443
478
|
|
|
444
479
|
# Returns the RCODE covering TSIG processing. Common values are
|
|
445
|
-
# NOERROR, BADSIG, BADKEY, and
|
|
480
|
+
# NOERROR, BADSIG, BADKEY, BADTIME and BADTRUNC (RFC 8945 section 5).
|
|
446
481
|
#
|
|
447
482
|
# print "error = ", rr.error, "\n"
|
|
448
483
|
#
|
|
@@ -471,6 +506,7 @@ module Dnsruby
|
|
|
471
506
|
@algorithm = DEFAULT_ALGORITHM
|
|
472
507
|
@fudge = DEFAULT_FUDGE
|
|
473
508
|
@mac_size = 0
|
|
509
|
+
@mac_length = nil
|
|
474
510
|
@mac = ""
|
|
475
511
|
@original_id = rand(65536)
|
|
476
512
|
@error = 0
|
|
@@ -479,10 +515,9 @@ module Dnsruby
|
|
|
479
515
|
@time_signed = nil
|
|
480
516
|
@buf = nil
|
|
481
517
|
|
|
482
|
-
#
|
|
518
|
+
# TSIG records use class ANY and TTL zero (RFC 8945 section 4.2).
|
|
483
519
|
@klass = Classes.ANY
|
|
484
|
-
|
|
485
|
-
@ttl = 0 # RFC 2845 Section 2.3
|
|
520
|
+
@ttl = 0
|
|
486
521
|
end
|
|
487
522
|
|
|
488
523
|
def from_data(data) #:nodoc: all
|
|
@@ -509,42 +544,29 @@ module Dnsruby
|
|
|
509
544
|
if (parts.length == 3)
|
|
510
545
|
return TSIG.new(parts[0], parts[1], parts[2]);
|
|
511
546
|
else
|
|
512
|
-
return TSIG.new(
|
|
547
|
+
return TSIG.new(DEFAULT_ALGORITHM, parts[0], parts[1]);
|
|
513
548
|
end
|
|
514
549
|
end
|
|
515
550
|
|
|
516
551
|
# Set the algorithm to use to generate the HMAC
|
|
517
552
|
# Supported values are :
|
|
518
|
-
# * hmac-md5
|
|
553
|
+
# * hmac-md5 (legacy; its use does not comply with RFC 8945)
|
|
519
554
|
# * hmac-sha1
|
|
520
555
|
# * hmac-sha256
|
|
521
556
|
# * hmac-sha512
|
|
522
557
|
def algorithm=(alg)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@algorithm = HMAC_MD5;
|
|
526
|
-
elsif (alg.downcase=="hmac-sha1")
|
|
527
|
-
@algorithm = HMAC_SHA1;
|
|
528
|
-
elsif (alg.downcase=="hmac-sha256")
|
|
529
|
-
@algorithm = HMAC_SHA256;
|
|
530
|
-
elsif (alg.downcase=="hmac-sha512")
|
|
531
|
-
@algorithm = HMAC_SHA512;
|
|
532
|
-
else
|
|
533
|
-
raise ArgumentError.new("Invalid TSIG algorithm")
|
|
534
|
-
end
|
|
535
|
-
elsif (alg.class == Name)
|
|
536
|
-
if (alg!=HMAC_MD5 && alg!=HMAC_SHA1 && alg!=HMAC_SHA256 && alg!=HMAC_SHA512)
|
|
537
|
-
raise ArgumentException.new("Invalid TSIG algorithm")
|
|
538
|
-
end
|
|
539
|
-
@algorithm=alg
|
|
540
|
-
else
|
|
541
|
-
raise ArgumentError.new("#{alg.class} not valid type for Dnsruby::RR::TSIG#algorithm= - use String or Name")
|
|
558
|
+
unless alg.is_a?(String) || alg.is_a?(Name)
|
|
559
|
+
raise ArgumentError, "#{alg.class} not valid for TSIG algorithm; use String or Name"
|
|
542
560
|
end
|
|
543
|
-
|
|
561
|
+
name = alg.to_s.downcase.sub(/\.$/, '')
|
|
562
|
+
name = HMAC_MD5.to_s.downcase.sub(/\.$/, '') if name == 'hmac-md5'
|
|
563
|
+
raise ArgumentError, 'Invalid TSIG algorithm' unless DIGESTS.key?(name)
|
|
564
|
+
@algorithm = Name.create(name + '.')
|
|
565
|
+
warn_if_legacy_algorithm
|
|
544
566
|
end
|
|
545
567
|
|
|
546
568
|
def fudge=(f)
|
|
547
|
-
if (f < 0 || f >
|
|
569
|
+
if (f < 0 || f > 0xFFFF)
|
|
548
570
|
@fudge = DEFAULT_FUDGE
|
|
549
571
|
else
|
|
550
572
|
@fudge = f
|
|
@@ -567,7 +589,8 @@ module Dnsruby
|
|
|
567
589
|
end
|
|
568
590
|
|
|
569
591
|
def encode_rdata(msg, canonical=false) #:nodoc: all
|
|
570
|
-
|
|
592
|
+
validate_wire_fields(self, @time_signed)
|
|
593
|
+
# Algorithm Name must be uncompressed (RFC 8945 section 4.2).
|
|
571
594
|
msg.put_name(@algorithm.downcase, true)
|
|
572
595
|
time_high = (@time_signed >> 32)
|
|
573
596
|
time_low = (@time_signed & 0xFFFFFFFF)
|
|
@@ -582,7 +605,19 @@ module Dnsruby
|
|
|
582
605
|
end
|
|
583
606
|
|
|
584
607
|
def self.decode_rdata(msg) #:nodoc: all
|
|
585
|
-
|
|
608
|
+
# RFC 8945 section 4.2 forbids compression in the algorithm name.
|
|
609
|
+
labels, length = [], 1
|
|
610
|
+
loop do
|
|
611
|
+
raise DecodeError, 'Truncated TSIG algorithm name' unless msg.has_remaining?
|
|
612
|
+
size, = msg.get_unpack('C')
|
|
613
|
+
break if size == 0
|
|
614
|
+
raise DecodeError, 'Compressed or invalid TSIG algorithm name' unless size && size <= 63
|
|
615
|
+
length += size + 1
|
|
616
|
+
raise DecodeError, 'TSIG algorithm name exceeds 255 octets' if length > 255
|
|
617
|
+
msg.assert_buffer_position_valid(msg.index + size)
|
|
618
|
+
labels << Name::Label.new(msg.get_bytes(size))
|
|
619
|
+
end
|
|
620
|
+
alg = Name.new(labels)
|
|
586
621
|
time_high, time_low = msg.get_unpack("nN")
|
|
587
622
|
time_signed = (time_high << 32) + time_low
|
|
588
623
|
fudge, = msg.get_unpack("n")
|