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.
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 +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  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 +986 -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 +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -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 +127 -14
  167. data/.coveralls.yml +0 -2
@@ -16,26 +16,28 @@
16
16
  module Dnsruby
17
17
  class RR
18
18
  # Class for CAA resource records.
19
- # RFC 6844
19
+ # RFC 8659
20
20
  class CAA < RR
21
21
  ClassValue = nil #:nodoc: all
22
22
  TypeValue= Types::CAA #:nodoc: all
23
23
 
24
- # The property tag for the record (issue|issuewild|issuemail|issuevmc|iodef|contactemail|contactphone)
24
+ # The property tag. Tags are extensible ASCII letters and digits.
25
25
  attr_accessor :property_tag
26
26
  # The value for the property_tag
27
27
  attr_accessor :property_value
28
28
  # The value for the flag
29
- attr_writer :flag
29
+ def flag=(value)
30
+ @flag = value.to_i
31
+ end
30
32
 
31
33
  def from_hash(hash) #:nodoc: all
32
- @property_tag = hash[:property_tag]
33
- @property_value = hash[:property_value]
34
- @flag = hash[:flag]
34
+ from_data([hash[:flag], hash[:property_tag], hash[:property_value]])
35
35
  end
36
36
 
37
37
  def from_data(data) #:nodoc: all
38
- @flag, @property_tag, @property_value = data
38
+ flag, @property_tag, @property_value = data
39
+ self.flag = flag
40
+ validate_fields
39
41
  end
40
42
 
41
43
  def flag
@@ -43,20 +45,31 @@ module Dnsruby
43
45
  end
44
46
 
45
47
  def from_string(input) #:nodoc: all
46
- matches = (/(\d+) (issuewild|issuemail|issuevmc|issue|iodef|contactemail|contactphone) "(.+)"$/i).match(input)
47
- if matches.nil?
48
- raise DecodeError.new("Cannot parse record: #{input[0...1000]}")
49
- end
50
- @flag = matches[1]
51
- @property_tag = matches[2]
52
- @property_value = matches[3]
48
+ # A CAA value is one character-string, with no 255-octet limit.
49
+ matches = /\A\s*(\d+)\s+([A-Za-z0-9]+)\s+("(?:\\.|[^"\\])*"|(?:\\.|[^\s"\\])+)\s*\z/m.match(input.b)
50
+ raise DecodeError, "Cannot parse CAA record: #{input[0...1000]}" unless matches
51
+
52
+ value = matches[3]
53
+ value = value[1...-1] if value.start_with?('"')
54
+ from_data([matches[1], matches[2], decode_value(value)])
53
55
  end
54
56
 
55
57
  def rdata_to_string #:nodoc: all
56
- "#{flag} #{@property_tag} \"#{@property_value}\""
58
+ value = +''.b
59
+ @property_value.each_byte do |octet|
60
+ if octet == 34 || octet == 92
61
+ value << 92 << octet
62
+ elsif octet < 32 || octet > 126
63
+ value << format('\\%03d', octet)
64
+ else
65
+ value << octet
66
+ end
67
+ end
68
+ "#{flag} #{@property_tag} \"#{value}\""
57
69
  end
58
70
 
59
71
  def encode_rdata(msg, canonical=false) #:nodoc: all
72
+ validate_fields
60
73
  msg.put_pack('C', flag)
61
74
  msg.put_string(@property_tag)
62
75
  # We don't put a length byte on the final string.
@@ -68,8 +81,46 @@ module Dnsruby
68
81
  property_tag = msg.get_string
69
82
  # The final string has no length byte - its length is implicit as the remainder of the packet length
70
83
  property_value = msg.get_bytes
71
- return self.new("#{flag} #{property_tag} \"#{property_value}\"")
84
+ record = self.new([flag, property_tag, property_value])
85
+ # Keep the public construction-time rdata String used by Comparable
86
+ # and older serializers, without reparsing arbitrary wire value bytes.
87
+ record.rdata = record.rdata_to_string
88
+ record
89
+ end
90
+
91
+ def validate_fields
92
+ unless (0..255).cover?(flag) && @property_tag.is_a?(String) &&
93
+ @property_tag.bytesize.between?(1, 255) && /\A[A-Za-z0-9]+\z/n.match?(@property_tag.b) &&
94
+ @property_value.is_a?(String)
95
+ raise DecodeError, 'CAA requires an octet flag, an alphanumeric tag and a string value'
96
+ end
97
+ end
98
+ private :validate_fields
99
+
100
+ def decode_value(value)
101
+ decoded = +''.b
102
+ index = 0
103
+ while index < value.bytesize
104
+ octet = value.getbyte(index)
105
+ if octet == 92
106
+ index += 1
107
+ octet = value.getbyte(index)
108
+ raise DecodeError, 'Incomplete CAA value escape' unless octet
109
+ if (48..57).cover?(octet)
110
+ digits = value.byteslice(index, 3)
111
+ unless /\A[0-9]{3}\z/.match?(digits) && digits.to_i <= 255
112
+ raise DecodeError, 'CAA decimal escapes must specify one octet with three digits'
113
+ end
114
+ octet = digits.to_i
115
+ index += 2
116
+ end
117
+ end
118
+ decoded << octet
119
+ index += 1
120
+ end
121
+ decoded
72
122
  end
123
+ private :decode_value
73
124
  end
74
125
  end
75
126
  end
@@ -22,6 +22,12 @@ module Dnsruby
22
22
  class DLV < RR::DS
23
23
  ClassValue = nil #:nodoc: all
24
24
  TypeValue = Types::DLV #:nodoc: all
25
+
26
+ # DLV owners are names within the lookaside registry, rather than the
27
+ # DNSKEY owner used in the digest. Keep that legacy distinction explicit.
28
+ def check_key(key)
29
+ key.type == Types.DNSKEY && key.klass == klass && matching_key_digest?(key)
30
+ end
25
31
  end
26
32
  end
27
33
  end
@@ -77,6 +77,7 @@ module Dnsruby
77
77
  begin
78
78
  alg = Algorithms.new(a)
79
79
  @algorithm = alg
80
+ @public_key = @key_length = nil
80
81
  rescue ArgumentError => e
81
82
  raise DecodeError.new(e)
82
83
  end
@@ -207,13 +208,13 @@ module Dnsruby
207
208
 
208
209
  def encode_rdata(msg, canonical=false) #:nodoc: all
209
210
  # 2 octets, then 2 sets of 1 octet
210
- msg.put_pack('ncc', @flags, @protocol, @algorithm.code)
211
+ msg.put_pack('nCC', @flags, @protocol, @algorithm.code)
211
212
  msg.put_bytes(@key)
212
213
  end
213
214
 
214
215
  def self.decode_rdata(msg) #:nodoc: all
215
216
  # 2 octets, then 2 sets of 1 octet
216
- flags, protocol, algorithm = msg.get_unpack('ncc')
217
+ flags, protocol, algorithm = msg.get_unpack('nCC')
217
218
  key = msg.get_bytes
218
219
  return self.new(
219
220
  [flags, protocol, algorithm, key])
@@ -292,9 +293,8 @@ module Dnsruby
292
293
 
293
294
  def key=(key_text)
294
295
  begin
295
- key_text.gsub!(/\n/, "")
296
- key_text.gsub!(/ /, "")
297
- @key=Base64.decode64(key_text)
296
+ @key=Base64.decode64(key_text.delete("\n "))
297
+ @public_key = @key_length = nil
298
298
  public_key
299
299
  get_new_key_tag
300
300
  rescue Exception => e
@@ -315,31 +315,121 @@ module Dnsruby
315
315
  @public_key = dsa_key
316
316
  elsif [Algorithms.ECDSAP256SHA256, Algorithms.ECDSAP384SHA384].include?(@algorithm)
317
317
  @public_key = ec_key(Algorithms.ECDSAP256SHA256 == @algorithm ? 'prime256v1' : 'secp384r1')
318
+ elsif @algorithm == Algorithms.ED25519 && DNSKEY.ed25519_supported?
319
+ @public_key = ed25519_key
320
+ elsif @algorithm == Algorithms.ED448 && DNSKEY.ed448_supported?
321
+ @public_key = ed448_key
318
322
  end
319
323
  end
320
324
  # @TODO@ Support other key encodings!
321
325
  return @public_key
322
326
  end
323
327
 
324
- def rsa_key
325
- exponentLength = @key[0]
326
- if (exponentLength.class == String)
327
- exponentLength = exponentLength.getbyte(0) # Ruby 1.9
328
+ # Check the actual provider as well as Ruby's OpenSSL API. Parsing a
329
+ # public key alone does not guarantee that signature verification works.
330
+ def self.ed25519_supported? #:nodoc:
331
+ return @ed25519_supported if defined?(@ed25519_supported)
332
+ return false unless defined?(OpenSSL::PKey)
333
+ public_bytes = ['d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'].pack('H*')
334
+ signature = ['e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155' \
335
+ '5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'].pack('H*')
336
+ # RFC 8032 section 7.1, test 1 (the empty message).
337
+ key = OpenSSL::PKey.read(ed25519_subject_public_key(public_bytes))
338
+ @ed25519_supported = key.verify(nil, signature, ''.b)
339
+ rescue OpenSSL::OpenSSLError, NotImplementedError, TypeError, ArgumentError
340
+ @ed25519_supported = false
341
+ end
342
+
343
+ def self.ed25519_subject_public_key(bytes) #:nodoc:
344
+ # RFC 8410 SubjectPublicKeyInfo: Ed25519's algorithm has no parameters.
345
+ OpenSSL::ASN1::Sequence.new([
346
+ OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::ObjectId.new('1.3.101.112')]),
347
+ OpenSSL::ASN1::BitString.new(bytes)
348
+ ]).to_der
349
+ end
350
+ private_class_method :ed25519_subject_public_key
351
+
352
+ def ed25519_key
353
+ unless @key.bytesize == 32
354
+ raise OpenSSL::PKey::PKeyError, 'Ed25519 DNSKEY must contain 32 octets'
355
+ end
356
+ @key_length = 256
357
+ OpenSSL::PKey.read(DNSKEY.send(:ed25519_subject_public_key, @key))
358
+ end
359
+
360
+ def self.ed448_supported? #:nodoc:
361
+ return @ed448_supported if defined?(@ed448_supported)
362
+ return false unless defined?(OpenSSL::PKey)
363
+ # RFC 8032 section 7.4, the empty-message test vector.
364
+ public_bytes = ['5fd7449b59b461fd2ce787ec616ad46a' \
365
+ '1da1342485a70e1f8a0ea75d80e96778' \
366
+ 'edf124769b46c7061bd6783df1e50f6c' \
367
+ 'd1fa1abeafe8256180'].pack('H*')
368
+ signature = ['533a37f6bbe457251f023c0d88f976ae' \
369
+ '2dfb504a843e34d2074fd823d41a591f' \
370
+ '2b233f034f628281f2fd7a22ddd47d78' \
371
+ '28c59bd0a21bfd3980ff0d2028d4b18a' \
372
+ '9df63e006c5d1c2d345b925d8dc00b41' \
373
+ '04852db99ac5c7cdda8530a113a0f4db' \
374
+ 'b61149f05a7363268c71d95808ff2e65' \
375
+ '2600'].pack('H*')
376
+ key = OpenSSL::PKey.read(ed448_subject_public_key(public_bytes))
377
+ @ed448_supported = key.verify(nil, signature, ''.b)
378
+ rescue OpenSSL::OpenSSLError, NotImplementedError, TypeError, ArgumentError
379
+ @ed448_supported = false
380
+ end
381
+
382
+ def self.ed448_subject_public_key(bytes) #:nodoc:
383
+ # RFC 8410: Ed448, like Ed25519, has no algorithm parameters.
384
+ OpenSSL::ASN1::Sequence.new([
385
+ OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::ObjectId.new('1.3.101.113')]),
386
+ OpenSSL::ASN1::BitString.new(bytes)
387
+ ]).to_der
388
+ end
389
+ private_class_method :ed448_subject_public_key
390
+
391
+ def ed448_key
392
+ unless @key.bytesize == 57
393
+ raise OpenSSL::PKey::PKeyError, 'Ed448 DNSKEY must contain 57 octets'
328
394
  end
395
+ @key_length = 448
396
+ OpenSSL::PKey.read(DNSKEY.send(:ed448_subject_public_key, @key))
397
+ end
398
+
399
+ def rsa_key
400
+ exponentLength = @key.getbyte(0)
329
401
  pos = 1
330
402
  if (exponentLength == 0)
331
- key1 = @key[1]
332
- if (key1.class == String) # Ruby 1.9
333
- key1 = key1.getbyte(0)
403
+ if @key.bytesize < 3
404
+ raise OpenSSL::PKey::PKeyError, "Truncated RSA DNSKEY exponent length"
405
+ end
406
+ exponentLength = @key.byteslice(1, 2).unpack1('n')
407
+ if exponentLength < 256
408
+ raise OpenSSL::PKey::PKeyError, "Noncanonical RSA DNSKEY exponent length"
334
409
  end
335
- exponentLength = (key1<<8) + key1
336
410
  pos += 2
337
411
  end
412
+ if !exponentLength || exponentLength == 0 || @key.bytesize <= pos + exponentLength
413
+ raise OpenSSL::PKey::PKeyError, "Truncated RSA DNSKEY exponent or modulus"
414
+ end
415
+ # RFC 3110 section 2 limits both integers to 4096 bits and forbids
416
+ # leading zero octets. Check bytes before constructing OpenSSL BNs.
417
+ modulus_length = @key.bytesize - pos - exponentLength
418
+ if exponentLength > 512 || modulus_length > 512 ||
419
+ @key.getbyte(pos) == 0 || @key.getbyte(pos + exponentLength) == 0
420
+ raise OpenSSL::PKey::PKeyError, "Invalid RSA DNSKEY integer encoding or size"
421
+ end
422
+ bits = (modulus_length - 1) * 8 + @key.getbyte(pos + exponentLength).bit_length
423
+ # RFC 3110 section 3 and RFC 5702 section 2 define modulus minima.
424
+ minimum = @algorithm == Algorithms.RSASHA512 ? 1024 : 512
425
+ if bits < minimum
426
+ raise OpenSSL::PKey::PKeyError, "RSA DNSKEY modulus must be at least #{minimum} bits"
427
+ end
338
428
  exponent = RR::get_num(@key[pos, exponentLength])
339
429
  pos += exponentLength
340
430
 
341
431
  modulus = RR::get_num(@key[pos, @key.length])
342
- @key_length = (@key.length - pos) * 8
432
+ @key_length = bits
343
433
 
344
434
  data_sequence = OpenSSL::ASN1::Sequence([
345
435
  OpenSSL::ASN1::Integer(modulus),
@@ -351,8 +441,10 @@ module Dnsruby
351
441
  end
352
442
 
353
443
  def dsa_key
354
- t = @key[0]
355
- t = t.getbyte(0) if t.class == String
444
+ t = @key.getbyte(0)
445
+ if !t || t > 8 || @key.bytesize != 21 + 3 * (t * 8 + 64)
446
+ raise OpenSSL::PKey::PKeyError, "Invalid DSA DNSKEY length"
447
+ end
356
448
  pgy_len = t * 8 + 64
357
449
  pos = 1
358
450
  q = RR::get_num(@key[pos, 20])
@@ -75,13 +75,10 @@ module Dnsruby
75
75
  end
76
76
 
77
77
  def DS.get_digest_type(d)
78
- if (d.instance_of?String)
79
- if (d.length == 1)
80
- d = d.to_i
81
- end
82
- end
78
+ d = d.to_i if d.is_a?(String) && /\A[0-9]+\z/.match?(d)
83
79
  begin
84
80
  digest = DigestTypes.new(d)
81
+ raise ArgumentError, 'DS digest type must fit in an unsigned octet' unless (0..255).cover?(digest.code)
85
82
  return digest
86
83
  rescue ArgumentError => e
87
84
  raise DecodeError.new(e)
@@ -89,13 +86,10 @@ module Dnsruby
89
86
  end
90
87
 
91
88
  def algorithm=(a)
92
- if (a.instance_of?String)
93
- if (a.length < 3)
94
- a = a.to_i
95
- end
96
- end
89
+ a = a.to_i if a.is_a?(String) && /\A[0-9]+\z/.match?(a)
97
90
  begin
98
91
  alg = Algorithms.new(a)
92
+ raise ArgumentError, 'DS algorithm must fit in an unsigned octet' unless (0..255).cover?(alg.code)
99
93
  @algorithm = alg
100
94
  rescue ArgumentError => e
101
95
  raise DecodeError.new(e)
@@ -130,24 +124,30 @@ module Dnsruby
130
124
 
131
125
  end
132
126
 
133
- # Check if the key's digest is the same as that stored in the DS record
127
+ # A DS binds both the DNSKEY's identity and its public-key digest.
134
128
  def check_key(key)
135
- if ((key.key_tag == @key_tag) && (key.algorithm == @algorithm))
129
+ key.type == Types.DNSKEY && key.klass == klass && key.name.canonical == name.canonical &&
130
+ matching_key_digest?(key)
131
+ end
136
132
 
137
- digestbin = digest_key(key)
138
- if (@digestbin == digestbin)
139
- if (!key.zone_key?)
140
- else
141
- return true
142
- end
143
- else
144
- end
145
- end
146
- return false
133
+ def matching_key_digest?(key)
134
+ key.zone_key? && key.key_tag == @key_tag && key.algorithm == @algorithm &&
135
+ !@digestbin.nil? && @digestbin == digest_key(key)
147
136
  end
137
+ protected :matching_key_digest?
148
138
 
149
139
 
150
- def DS.from_key(key, digest_type)
140
+ # Create a DS for current delegation use. Historical records can still
141
+ # be decoded and checked with digest_key/check_key, but RFC 9905 prohibits
142
+ # new SHA-1 delegations and DS records for RSA/SHA-1 signing keys.
143
+ def DS.from_key(key, digest_type = 2)
144
+ if [Algorithms.RSASHA1, Algorithms.RSASHA1_NSEC3_SHA1].include?(key.algorithm)
145
+ raise ArgumentError, "RFC 9905 prohibits creating DS records for RSA/SHA-1 keys"
146
+ end
147
+ digest_type = get_digest_type(digest_type)
148
+ if digest_type.code == 1
149
+ raise ArgumentError, "RFC 9905 prohibits SHA-1 for new DS delegations; use SHA-256 or SHA-384"
150
+ end
151
151
  # # The key must not be a NULL key.
152
152
  # if ((key.flags & 0xc000 ) == 0xc000 )
153
153
  # puts "\nCreating a DS record for a NULL key is illegal"
@@ -175,9 +175,8 @@ module Dnsruby
175
175
  # return
176
176
  # end
177
177
  #
178
- digest_type = get_digest_type(digest_type)
179
178
  # Create a new DS record from the specified key
180
- ds = RR.create(:name => key.name, :type => "DS", :ttl => key.ttl,
179
+ ds = RR.create(:name => key.name, :type => "DS", :klass => key.klass, :ttl => key.ttl,
181
180
  :key_tag => key.key_tag,
182
181
  :digest_type => digest_type, :algorithm => key.algorithm)
183
182
 
@@ -238,16 +237,16 @@ module Dnsruby
238
237
  end
239
238
 
240
239
  def encode_rdata(msg, canonical=false) #:nodoc: all
241
- msg.put_pack("ncc", @key_tag, @algorithm.code, @digest_type.code)
240
+ msg.put_pack("nCC", @key_tag, @algorithm.code, @digest_type.code)
242
241
  msg.put_bytes(@digestbin)
243
242
  end
244
243
 
245
244
  def self.decode_rdata(msg) #:nodoc: all
246
- key_tag, algorithm, digest_type = msg.get_unpack("ncc")
245
+ key_tag, algorithm, digest_type = msg.get_unpack("nCC")
247
246
  digest = msg.get_bytes
248
247
  return self.new(
249
248
  [key_tag, algorithm, digest_type, digest])
250
249
  end
251
250
  end
252
251
  end
253
- end
252
+ end
@@ -0,0 +1,16 @@
1
+ require 'dnsruby/resource/SVCB'
2
+
3
+ module Dnsruby
4
+ class RR
5
+ # Class for DNS HTTPS resource records.
6
+ #
7
+ # RFC 9460 sec 9
8
+ #
9
+ # The HTTPS-specific instantiation of the SVCB record, sharing its wire and
10
+ # presentation formats, for the "https" and "http" URI schemes.
11
+ class HTTPS < SVCB
12
+ ClassValue = nil #:nodoc: all
13
+ TypeValue = Types::HTTPS #:nodoc: all
14
+ end
15
+ end
16
+ end
@@ -74,6 +74,25 @@ module Dnsruby
74
74
  self.const_set(s.name.sub(/.*::/, ''), c)
75
75
  }
76
76
 
77
+ # RFC 9460 defines these formats only for class IN. Other ordinary
78
+ # classes must retain their opaque RDATA through the generic fallback.
79
+ class SVCB < RR::SVCB
80
+ ClassHash[[TypeValue = Types::SVCB, ClassValue = ClassValue]] = self #:nodoc: all
81
+ end
82
+
83
+ class HTTPS < RR::HTTPS
84
+ ClassHash[[TypeValue = Types::HTTPS, ClassValue = ClassValue]] = self #:nodoc: all
85
+ end
86
+
87
+ # RFC 2136 uses ANY/NONE as UPDATE markers, not as an RDATA format.
88
+ # Keep the IN codec (including empty deletes/prerequisites), with the
89
+ # actual class stored on the record, just as Update#delete does.
90
+ [SVCB, HTTPS].each do |record_class|
91
+ [Classes::ANY, Classes::NONE].each do |update_class|
92
+ ClassHash[[record_class::TypeValue, update_class]] = record_class
93
+ end
94
+ end
95
+
77
96
  # RFC 1035, Section 3.4.2 (deprecated)
78
97
  class WKS < RR
79
98
  ClassHash[[TypeValue = Types::WKS, ClassValue = ClassValue]] = self #:nodoc: all
@@ -87,13 +106,13 @@ module Dnsruby
87
106
 
88
107
  def encode_rdata(msg, canonical=false) #:nodoc: all
89
108
  msg.put_bytes(@address.address)
90
- msg.put_pack("n", @protocol)
109
+ msg.put_pack("C", @protocol)
91
110
  msg.put_bytes(@bitmap)
92
111
  end
93
112
 
94
113
  def self.decode_rdata(msg) #:nodoc: all
95
114
  address = IPv4.new(msg.get_bytes(4))
96
- protocol, = msg.get_unpack("n")
115
+ protocol, = msg.get_unpack("C")
97
116
  bitmap = msg.get_bytes
98
117
  return self.new(address, protocol, bitmap)
99
118
  end
@@ -37,7 +37,7 @@ module Dnsruby
37
37
 
38
38
  def from_string(input) #:nodoc: all
39
39
  if (input.length > 0)
40
- names = input.split(" ")
40
+ names = Presentation.tokens(input)
41
41
  if(names.size != 2)
42
42
  raise DecodeError.new("MX record expects preference and domain")
43
43
  end
@@ -66,4 +66,4 @@ module Dnsruby
66
66
  end
67
67
  end
68
68
  end
69
- end
69
+ end
@@ -236,14 +236,10 @@ module Dnsruby
236
236
 
237
237
  def from_string(input)
238
238
  if input.length > 0
239
- data = input.split(' ')
240
- self.next_domain = data[0]
241
- len = data[0].length+ 1
242
- if data[1] == '('
243
- len += data[1].length
244
- end
245
- self.types = input[len, input.length-len]
246
- @types = NSEC.get_types(input[len, input.length-len])
239
+ data = Presentation.tokens(input)
240
+ self.next_domain = data.shift
241
+ data.shift if data.first == '('
242
+ self.types = data.join(' ')
247
243
  end
248
244
  end
249
245
 
@@ -260,7 +256,7 @@ module Dnsruby
260
256
 
261
257
  def encode_rdata(msg, canonical=false) #:nodoc: all
262
258
  # Canonical
263
- msg.put_name(@next_domain, canonical, false) # dnssec-bis-updates says NSEC should not be downcased
259
+ msg.put_name(@next_domain, true, false) # dnssec-bis-updates says NSEC should not be downcased
264
260
  types = encode_types
265
261
  msg.put_bytes(types)
266
262
  end
@@ -272,4 +268,4 @@ module Dnsruby
272
268
  end
273
269
  end
274
270
  end
275
- end
271
+ end
@@ -90,14 +90,46 @@ module Dnsruby
90
90
  # NSEC RR's owner name
91
91
  attr_reader :types
92
92
 
93
- def check_name_in_range(_name)
94
- # @TODO@ Check if the name is covered by this record
95
- false
93
+ # A hash interval proves absence, while equality proves existence. Keep
94
+ # these operations separate: an interval's bitmap describes its owner,
95
+ # never a name merely covered by the interval (RFC 5155 sections 8.3-8.7).
96
+ def supported_parameters? # :nodoc:
97
+ @hash_alg == Nsec3HashAlgorithms.SHA_1 && [0, 1].include?(@flags)
96
98
  end
97
99
 
98
- def check_name_in_wildcard_range(_name)
99
- # @TODO@ Check if the name is covered by this record
100
- false
100
+ def usable_hash? # :nodoc:
101
+ supported_parameters? &&
102
+ @iterations.is_a?(Integer) && @iterations.between?(0, 65_535) &&
103
+ @next_hashed.is_a?(String) && @next_hashed.bytesize == 20 &&
104
+ /\A[0-9a-v]{32}\z/.match?(owner_hash)
105
+ end
106
+
107
+ def owner_hash # :nodoc:
108
+ @name.labels.first.to_s.downcase
109
+ end
110
+
111
+ def hash_name(name) # :nodoc:
112
+ NSEC3.calculate_hash(name, @iterations, @salt, @hash_alg)
113
+ end
114
+
115
+ def covers_hash?(hashed) # :nodoc:
116
+ return false unless usable_hash?
117
+ first, following = owner_hash, NSEC3.encode_next_hashed(@next_hashed)
118
+ return false if hashed == first || hashed == following
119
+ first < following ? first < hashed && hashed < following : first < hashed || hashed < following
120
+ end
121
+
122
+ def check_name_in_range(name)
123
+ candidate = Name.create(name)
124
+ zone = @name.strip_label
125
+ usable_hash? && (candidate == zone || candidate.subdomain_of?(zone)) && covers_hash?(hash_name(candidate))
126
+ end
127
+
128
+ # The closest encloser must be established by the complete proof before
129
+ # checking its literal wildcard name; one NSEC3 cannot discover it alone.
130
+ def check_name_in_wildcard_range(name)
131
+ candidate = Name.create(name)
132
+ candidate.wild? && check_name_in_range(candidate)
101
133
  end
102
134
 
103
135
  def calculate_hash
@@ -138,7 +170,12 @@ module Dnsruby
138
170
  n = Name.create(name)
139
171
  out = n.canonical
140
172
  begin
141
- (iterations + 1).times { out = NSEC3.h(out + salt, hash_alg) }
173
+ budget = ValidationBudget.current
174
+ budget&.check!(iterations + 1)
175
+ (iterations + 1).times do |iteration|
176
+ budget&.check! if iteration % 64 == 0
177
+ out = NSEC3.h(out + salt, hash_alg)
178
+ end
142
179
  return Base32.encode32hex(out).downcase
143
180
  rescue ArgumentError
144
181
  TheLog.error("Unknown hash algorithm #{hash_alg} used for NSEC3 hash")
@@ -181,11 +218,12 @@ module Dnsruby
181
218
 
182
219
  OPT_OUT = 1
183
220
  def flags=(f)
184
- if f == 0 || f == OPT_OUT
185
- @flags = f
186
- else
187
- raise DecodeError.new("Unknown NSEC3 flags field - #{f}")
221
+ unless f.is_a?(Integer) && f.between?(0, 255)
222
+ raise DecodeError, "NSEC3 flags must be an octet: #{f}"
188
223
  end
224
+ # Preserve future flags on decode. Validators ignore unsupported
225
+ # records under RFC 5155 section 8.2 instead of rejecting the message.
226
+ @flags = f
189
227
  end
190
228
 
191
229
  # If the Opt-Out flag is set, the NSEC3 record covers zero or more
@@ -216,6 +254,7 @@ module Dnsruby
216
254
  # self.salt_length=(salt_length)
217
255
  # self.salt=(salt)
218
256
  @salt = salt
257
+ @salt_length = salt.bytesize
219
258
  self.hash_length = hash_length
220
259
  self.next_hashed = next_hashed
221
260
  self.types = types
@@ -301,27 +340,24 @@ module Dnsruby
301
340
  # s = salt()
302
341
  s = @salt
303
342
  sl = s.length
304
- if s == '-'
305
- sl = 0
306
- end
307
- msg.put_pack('ccnc', @hash_alg.code, @flags, @iterations, sl)
343
+ msg.put_pack('CCnC', @hash_alg.code, @flags, @iterations, sl)
308
344
  if sl > 0
309
345
  msg.put_bytes(s)
310
346
  end
311
- msg.put_pack('c', @hash_length)
347
+ msg.put_pack('C', @hash_length)
312
348
  msg.put_bytes(@next_hashed)
313
349
  types = NSEC.encode_types(self)
314
350
  msg.put_bytes(types)
315
351
  end
316
352
 
317
353
  def self.decode_rdata(msg) #:nodoc: all
318
- hash_alg, flags, iterations, salt_length = msg.get_unpack('ccnc')
354
+ hash_alg, flags, iterations, salt_length = msg.get_unpack('CCnC')
319
355
  # Salt may be omitted
320
- salt = []
356
+ salt = ''.b
321
357
  if salt_length > 0
322
358
  salt = msg.get_bytes(salt_length)
323
359
  end
324
- hash_length, = msg.get_unpack('c')
360
+ hash_length, = msg.get_unpack('C')
325
361
  next_hashed = msg.get_bytes(hash_length)
326
362
  types = NSEC.decode_types(msg.get_bytes)
327
363
  return self.new(