dnsruby 1.74.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +302 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/demo/rubydig.rb +0 -2
  10. data/dnsruby.gemspec +5 -4
  11. data/lib/dnsruby/cache.rb +279 -47
  12. data/lib/dnsruby/code_mapper.rb +35 -8
  13. data/lib/dnsruby/code_mappers.rb +63 -49
  14. data/lib/dnsruby/config.rb +75 -4
  15. data/lib/dnsruby/deadline_queue.rb +75 -0
  16. data/lib/dnsruby/dnssec.rb +148 -45
  17. data/lib/dnsruby/ipv6.rb +45 -5
  18. data/lib/dnsruby/key_cache.rb +202 -35
  19. data/lib/dnsruby/message/decoder.rb +85 -19
  20. data/lib/dnsruby/message/encoder.rb +27 -16
  21. data/lib/dnsruby/message/header.rb +8 -12
  22. data/lib/dnsruby/message/message.rb +53 -22
  23. data/lib/dnsruby/message/section.rb +12 -34
  24. data/lib/dnsruby/name.rb +5 -2
  25. data/lib/dnsruby/packet_sender.rb +645 -203
  26. data/lib/dnsruby/presentation.rb +147 -0
  27. data/lib/dnsruby/query_control.rb +132 -0
  28. data/lib/dnsruby/recursor.rb +795 -339
  29. data/lib/dnsruby/resolver.rb +468 -186
  30. data/lib/dnsruby/resource/CAA.rb +67 -16
  31. data/lib/dnsruby/resource/DLV.rb +6 -0
  32. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  33. data/lib/dnsruby/resource/DS.rb +27 -28
  34. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  35. data/lib/dnsruby/resource/IN.rb +19 -0
  36. data/lib/dnsruby/resource/MX.rb +2 -2
  37. data/lib/dnsruby/resource/NSEC.rb +6 -10
  38. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  39. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +979 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +42 -20
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +21 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +105 -10
  167. data/.coveralls.yml +0 -2
@@ -21,18 +21,10 @@ rescue LoadError
21
21
  end
22
22
  module Dnsruby
23
23
  class RR
24
- # TSIG implements RFC2845.
25
- #
26
- # "This protocol allows for transaction level authentication using
27
- # shared secrets and one way hashing. It can be used to authenticate
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 = HMAC_MD5
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 (RFC2845 3.4.1)
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
- # # Add the request MAC if present (used to validate responses).
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
- # Original ID - should we set message ID to original ID?
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
- # If msg_bytes came in, we need somehow to remove the TSIG RR
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
- :name => tsig_rr.name,
128
- :type => Types.TSIG,
129
- :ttl => tsig_rr.ttl,
130
- :klass => tsig_rr.klass,
131
- :algorithm => tsig_rr.algorithm,
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 calculate_mac(algorithm, data)
145
- mac=nil
146
- # + if (key_size > max_digest_len) {
147
- # + EVP_DigestInit(&ectx, digester);
148
- # + EVP_DigestUpdate(&ectx, (const void*) key_bytes, key_size);
149
- # + EVP_DigestFinal(&ectx, key_bytes, NULL);
150
- # + key_size = max_digest_len;
151
- # + }
152
- key = @key.gsub(" ", "")
153
- # key = Base64::decode64(key)
154
- key = key.unpack("m*")[0]
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. This method will be called by Dnsruby::SingleResolver
188
- # before passing a response to the client code.
189
- # The TSIG record will be removed from packet before passing to client, and
190
- # the Message#tsigstate and Message#tsigerror will be set accordingly.
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
- # 4.6. Client processing of answer
196
- #
197
- # When a client receives a response from a server and expects to see a
198
- # TSIG, it first checks if the TSIG RR is present in the response.
199
- # Otherwise, the response is treated as having a format error and
200
- # discarded. The client then extracts the TSIG, adjusts the ARCOUNT,
201
- # and calculates the keyed digest in the same way as the server. If
202
- # the TSIG does not validate, that response MUST be discarded, unless
203
- # the RCODE is 9 (NOTAUTH), in which case the client SHOULD attempt to
204
- # verify the response as if it were a TSIG Error response, as specified
205
- # in [4.3]. A message containing an unsigned TSIG record or a TSIG
206
- # record which fails verification SHOULD not be considered an
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
- new_msg_tsig_rr = generate(response, query, buf, response_bytes, msg_tsig_rr)
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 verify_common(response)#:nodoc: all
235
- tsig_rr = response.tsig
268
+ def verification_failed(response, error)
269
+ response.tsigstate = :Failed
270
+ response.tsigerror = error
271
+ false
272
+ end
236
273
 
237
- if (!tsig_rr)
238
- response.tsigerror = RCode.FORMERR
239
- response.tsigstate = :Failed
240
- return false
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
- response.additional.delete(tsig_rr)
244
- response.header.arcount-=1
245
-
246
- # First, check the TSIG error in the RR
247
- if (tsig_rr.error != RCode.NOERROR)
248
- response.tsigstate = :Failed
249
- response.tsigerror = tsig_rr.error
250
- return false
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
- if ((tsig_rr.name != @name) || (tsig_rr.algorithm.downcase != @algorithm.downcase))
254
- Dnsruby.log.error("BADKEY failure")
255
- response.tsigstate = :Failed
256
- response.tsigerror = RCode.BADKEY
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
- # Check time_signed (RFC2845, 4.5.2) - only really necessary for server
261
- if (Time.now.to_i > tsig_rr.time_signed + tsig_rr.fudge ||
262
- Time.now.to_i < tsig_rr.time_signed - tsig_rr.fudge)
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
- return true
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
- # Checks TSIG signatures across sessions of multiple DNS envelopes.
273
- # This method is called each time a new envelope comes in. The envelope
274
- # is checked - if a TSIG is present, them the stream so far is verified,
275
- # and the response#tsigstate set to :Verified. If a TSIG is not present,
276
- # and does not need to be present, then the message is added to the digest
277
- # stream and the response#tsigstate is set to :Intermediate.
278
- # If there is an error with the TSIG verification, then the response#tsigstate
279
- # is set to :Failed.
280
- # Like verify, this method will only be called by the Dnsruby::SingleResolver
281
- # class. Client code need not call this method directly.
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 = @buf + response_bytes
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
- # Now add the current message data - remember to frig the arcount
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
- @buf += timers_data
369
-
370
- mac = calculate_mac(tsig.algorithm, @buf)
371
-
372
- if (mac != tsig.mac)
373
- Dnsruby.log.error("TSIG Verify error on TSIG TCP session")
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', mac.length)
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
- # The only algorithms currently supported are hmac-md5 and hmac-sha1.
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
- # The programmer must call a Net::DNS::Packet object's data method
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
- # Returns the message authentication code (MAC) as a string of hex
430
- # characters. The programmer must call a Net::DNS::Packet object's
431
- # data method before this will return anything meaningful.
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 BADTIME. See RFC 2845 for details.
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
- # RFC 2845 Section 2.3
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(HMAC_MD5, parts[0], parts[1]);
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
- if (alg.class == String)
524
- if (alg.downcase=="hmac-md5")
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
- Dnsruby.log.debug{"Using #{@algorithm.to_s} algorithm"}
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 > 0x7FFF)
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
- # Name needs to be added with no compression - done in Dnsruby::Message#encode
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
- alg=msg.get_name
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")