DNSフルリゾルバの実装への DNSSEC の組み込み – NSEC/NSEC3 による否定応答の証明
2024年04月24日 水曜日
CONTENTS
研究開発中のフルリゾルバ実装の DNSSEC検証機能の完成度を向上する目的で、否定応答の証明機能を実装しました。
否定応答の証明について説明した後に実装について紹介します。
DNSSECにおける否定応答
権威サーバからの応答内容の正しさを署名検証で確認するためには、検証の対象となるデータが必要です。
DNSSECの署名検証では、RRset が必要となります。
しかし、Name Error や No Data といった否定応答では、返却するべきデータが空である(RRset が無い)ため、単純には実現できません。
そこで、ゾーン内でのドメイン名の範囲情報を表現する NSEC/NSEC3 レコードを利用し、ドメイン名の存在あるいは範囲内にドメイン名が存在しないことを表現します。
NSEC/NSEC3 による範囲情報
NSECレコードおよび NSEC3レコードは、ゾーン内の存在するドメイン名と正規順序1を利用して、範囲情報を表現しています。
例えば下図(図: ドメイン名と正規順序とNSECレコード)の “example.” ゾーンでは、
ドメイン名 “gov.example.” の NSEC レコードは次ドメイン名フィールド2として “ex.gov.example.” を持ちます。
これは、”example.” ゾーン内をドメイン名正規順序で並べたときに、 “gov.example.” の次のドメイン名が “ex.gov.example.” であって、その間にはドメイン名が存在しないことを表しています。
つまり、下限 “gov.example.”、上限 “ex.gov.example.” の範囲情報と解釈できます。
ゾーン全体はNSECレコードの範囲情報によって、排他的に分割されて、覆われます。
下図では、 “ex2.gov.example.” はドメイン名正規順序でゾーン内最大のドメイン名です。
ゾーン内最大のドメイン名を下限とする範囲情報は、他の範囲とは異なり、上限にはゾーン頂点のドメイン名(下図では “example.”)が指定されます。
NSECでは、ある範囲の上限を用いると、それが下限となる次の範囲を指定できます。このように順に範囲を辿っていくと、ゾーン内に存在するドメイン名をすべて入手できます。
NSECのこの性質は、ゾーン内に公開したくないドメイン名がある場合に問題となります。
そこで、NSEC3 では容易にはドメイン名を列挙できないようにするために、ハッシュ化したドメイン名を利用します(図: NSEC3のハッシュ化後ドメイン名とNSEC3レコード)。
NSEC3 の場合も、ゾーン全体が排他的に分割されて、覆われるのは同じですが、詳細がいくつか異なります。
- ドメイン名のハッシュ化後に正規順序を適用します(NSEC3のハッシュ化ではハッシュ関数を適用した後に Base32Hex符号化34を行ないます)。
- NSECでは、ドメイン名正規順序で、ゾーン頂点がゾーン内で最小のドメイン名でしたが、ハッシュ化後ではそうではありません。ゾーン最大のドメイン名を下限とする範囲の上限は、ハッシュ化後に最小となるドメイン名(下図では “H(ns.ex.gov.example.).example.”)が指定されます。
- Opt-Out5により、対応する NSEC3レコードが生成されていないドメイン名が存在することがあります(図: NSEC3レコードとOut-Outによる生成の省略)。
しかし、ゾーン全体が排他的に分割されている性質は変わらないため、次の性質が成立します。
- ゾーン内の存在しない名前に対して、その名前を覆う NSEC/NSEC3 レコードが一意に決まります。
- ゾーン内の存在する名前に対して、その名前と一致する NSEC/NSEC3 レコードが一意に決まります。
NSEC/NSEC3 で表現できる 5通りの証明
範囲情報を利用することで、
- Name Error(ドメイン名の非存在)
- No Data(目的のタイプのレコード無し)
- Referral to Unsigned Zone(未署名ゾーンへの参照)
- Wildcard Expansion(ワイルドカードへの一致、レコード有り)
- Wildcard No Data(ワイルドカードへの一致、レコード無し)
の5通りの証明を表現することができます。
より具体的にはドメイン名が存在する証拠あるいは存在しない証拠を複数組み合わせます。
NSECレコードでの、それぞれの場合に応じた証拠は次の通りです。
- Name Error
- 証拠1: 問い合わせたドメイン名が存在しない(問い合せたドメイン名を覆う NSEC レコード)
- 証拠2: 展開で一致するワイルドカードドメイン名が存在しない(ワイルドカードドメイン名を覆う NSEC レコード)
- No Data
- 証拠: 問い合せたドメイン名が存在する(一致する NSEC レコード)
- Referral to Unsigned Zone
- 証拠: 問い合せたドメイン名が存在しない(問い合せたドメイン名を覆う NSEC レコード)
- Wildcard Expansion
- 証拠: ワイルドカード展開よりも具体的に一致するドメイン名が存在しない(具体的に一致するドメイン名を覆う NSEC レコード)
- Wildcard No Data
- 証拠1: 展開で一致するワイルドカードドメイン名が存在する(ワイルドカードドメイン名と一致する NSEC レコード)
- 証拠2: ワイルドカード展開よりも具体的に一致するドメイン名が存在しない(具体的に一致するドメイン名を覆う NSEC レコード)
NSEC3レコードの場合はハッシュ化されたドメイン名を扱うため、証拠も若干複雑になります。
ここで、問い合せたドメイン名の親ドメイン名(ゾーン内)のうち、
存在しない最も短いものを次近接名、
存在する最も長いものを最近接名と呼びます。
結果的に、最近接名は次近接名から先頭のラベルを取り除いたものになります。
また、最近接名の先頭にワイルドカードラベル(”*”)を加えたドメイン名をワイルドカード名と呼ぶことにします。
例えば、下図(再掲:図:NSEC3のハッシュ化後のドメイン名とNSEC3レコード)の “example.” ゾーンに対してドメイン名 “a.b.gov.example.” の問い合わせを行なったとすると、次近接名は “b.gov.example.”、最近接名は “gov.example.”、ワイルドカード名は “*.gov.example.” となります。
NSEC3レコードでの、それぞれの場合に応じた証拠は次の通りです。
- Name Error
- 証拠1: 最近接名が存在する(最近接名に一致する NSEC3 レコード)
- 証拠2: 次近接名が存在しない(次近接名を覆う NSEC3 レコード)
- 証拠3: ワイルドカード名が存在しない(ワイルドカード名を覆う NSEC3 レコード)
- No Data
- 証拠: 最近接名が存在する(最近接名に一致する NSEC3 レコード)
- Referral to Unsigned Zone
- 証拠1: 最近接名が存在する(最近接名に一致する NSEC3 レコード)
- 証拠2: 次近接名が存在しない(次近接名を覆う NSEC3 レコード)
- Wildcard Expansion
- 証拠: 次近接名が存在しない(次近接名を覆う NSEC3 レコード)
- Wildcard No Data
- 証拠1: 最近接名に一致する NSEC3 レコード(最近接名が存在する)
- 証拠2: 次近接名を覆う NSEC3 レコード(次近接名が存在しない)
- 証拠3: ワイルドカード名が存在する(ワイルドカード名に一致する NSEC3 レコード)
それぞれのNSEC/NSEC3 レコードの正当性は、ゾーンの DNSKEY を使った署名検証によって確認します。
フルリゾルバ実装 bowline への否定応答証明の組み込み
フルリゾルバ実装bowline6 に NSEC/NSEC3 による否定応答の証明を組み込みました。
まず、ライブラリdnsext-dnssec7では、5通りの状況の、それぞれの証拠を取得できるかを個別に判定しています。
当初の実装では 5通りの状況を自動判別する機能のみを実装していました。
しかし、5通りの場合の排他条件が常に成立しない場合があるのと、排他でない場合には意図とは異なる判別結果になる可能性があることを考慮して、個別の判定へと変更しました。
たとえば、NSEC3 の Name Error についての証拠は次のようなデータ型になります。
NSEC3_Range は NSEC3 の範囲情報を表現する型です。それぞれのフィールドは
- nsec3_nameError_closest_match: 最近接名と、その名前に一致する範囲情報
- nsec3_nameError_next_closer_cover: 次近接名と、その名前を覆う範囲情報
- nsec3_nameError_wildcard_cover: ワイルドカード名と、その名前を覆う範囲情報
を保持することになります。
data NSEC3_NameError = NSEC3_NameError { nsec3_nameError_closest_match :: (NSEC3_Range, Domain) , nsec3_nameError_next_closer_cover :: (NSEC3_Range, Domain) , nsec3_nameError_wildcard_cover :: (NSEC3_Range, Domain) }
また、NSEC3 ではハッシュ化後の正規順序による判定を行ないますが、ここで Base32Hex 符号化前後でドメイン名正規順序が保たれる性質4をうまく利用することができます。
NSEC3 の Owner Name は Base32Hex 符号化された文字列ですが、Next Hashed Owner Name は符号化されていないハッシュ値です。
つまり、Owner Name を Base32Hex で復号化したものを下限、 Next Hashed Owner Name を上限とする範囲情報を利用すれば良いので、
「一致する」かの判定、あるいは「覆う」かの判定、の処理には Base32Hex 符号化は必要無く、Owner Name の Base32Hex 復号化のみで良いことがわかります。
そして、フルリゾルバへの組み込みです。
反復検索での委任情報取得時には、「Referral to Unsigned Zone 」の証拠の取得を行ないます。
また、最終結果取得時には、次のように場合分けをして証拠の取得を行ないます。
- No Data のとき 「No Data」および「Wildcard No Data」の証拠の取得
- Name Error のとき 「Name Error」の証拠の取得
- 問い合せの結果があるとき 「Wildcard Expansion」の証拠の取得(ワイルドカードへの一致の結果とは限らないので取得できない場合もあります。)
最後にdugコマンド8による否定応答の実行例を示します。
mail.dns-oarc.net. NS の問い合せで NSEC の No Data の証明の様子が、
また、does-not-exist.iij.ad.jp. A の問い合わせで NSEC3 の Name Error の証明の様子が確認できます。
% dug -v 1 -i mail.dns-oarc.net. NS +dnssec ... delegation - verification success - RRSIG of DS: "." -> "net." ... delegation - verification success - RRSIG of DS: "net." -> "dns-oarc.net." ... no delegation: "dns-oarc.net." -> "mail.dns-oarc.net." ... nsec verification success - NSEC NoData: "mail.dns-oarc.net." NS ;; 228usec ;; HEADER SECTION: ;Standard query, NoError, id: 0 ;Flags: Recursion Desired, Recursion Available ;; QUESTION SECTION: ;mail.dns-oarc.net. IN NS ;; ANSWER SECTION: ;; AUTHORITY SECTION: dns-oarc.net. 300(5 mins) IN SOA RD_SOA {soa_mname = "ns1.dns-oarc.net.", soa_rname = "hostmaster@dns-oarc.net.", soa_serial = 2024032200, soa_refresh = 300(5 mins), soa_retry = 60(1 min), soa_expire = 604800(7 days), soa_minimum = 3600(1 hour)} dns-oarc.net. 300(5 mins) IN RRSIG RD_RRSIG {rrsig_type = SOA, rrsig_pubalg = ECDSAP256SHA256, rrsig_num_labels = 2, rrsig_ttl = 300(5 mins), rrsig_expiration = Fri, 05 Apr 2024 04:51:30 GMT, rrsig_inception = Fri, 22 Mar 2024 03:21:30 GMT, rrsig_key_tag = 6048, rrsig_zone = "dns-oarc.net.", rrsig_signature = \# 64 8e8e6b09aec6cd552d88780ab8fbb4d89d3f4688466303fb34953b27394a30683bcb9ac33fb088e90a48a30b3c8c80504a0b9c993ecfece3df554a2d0bcb8dc1} mail.dns-oarc.net. 300(5 mins) IN NSEC RD_NSEC {nsec_next_domain = "maint.dns-oarc.net.", nsec_types = [A,RRSIG,NSEC]} mail.dns-oarc.net. 300(5 mins) IN RRSIG RD_RRSIG {rrsig_type = NSEC, rrsig_pubalg = ECDSAP256SHA256, rrsig_num_labels = 3, rrsig_ttl = 300(5 mins), rrsig_expiration = Thu, 04 Apr 2024 15:50:10 GMT, rrsig_inception = Thu, 21 Mar 2024 14:20:10 GMT, rrsig_key_tag = 6048, rrsig_zone = "dns-oarc.net.", rrsig_signature = \# 64 9cd26cda7ffef1d82e1981cebf95a636427306dbc5c2a12087f2c179b016a4796db95912a80c3d4bfbe8d1c6ecd18689f7de6a7fd904874548fce82cfe46ba3f} ;; ADDITIONAL SECTION:
% dug -v 1 -i does-not-exist.iij.ad.jp. A +dnssec ... delegation - verification success - RRSIG of DS: "." -> "jp." ... no delegation: "jp." -> "ad.jp." ... delegation - verification success - RRSIG of DS: "jp." -> "iij.ad.jp." ... no delegation: "iij.ad.jp." -> "does-not-exist.iij.ad.jp." ... nsec verification success - NSEC3 NameError: "does-not-exist.iij.ad.jp." A ;; 53usec ;; HEADER SECTION: ;Standard query, NXDomain, id: 0 ;Flags: Recursion Desired, Recursion Available ;; QUESTION SECTION: ;does-not-exist.iij.ad.jp. IN A ;; ANSWER SECTION: ;; AUTHORITY SECTION: iij.ad.jp. 3600(1 hour) IN SOA RD_SOA {soa_mname = "bh0.iij.ad.jp.", soa_rname = "postmaster@iij.ad.jp.", soa_serial = 1711037404, soa_refresh = 3600(1 hour), soa_retry = 1800(30 mins), soa_expire = 1209600(14 days), soa_minimum = 3600(1 hour)} iij.ad.jp. 3600(1 hour) IN RRSIG RD_RRSIG {rrsig_type = SOA, rrsig_pubalg = RSASHA256, rrsig_num_labels = 3, rrsig_ttl = 86400(1 day), rrsig_expiration = Sat, 20 Apr 2024 15:10:04 GMT, rrsig_inception = Thu, 21 Mar 2024 15:10:04 GMT, rrsig_key_tag = 54096, rrsig_zone = "iij.ad.jp.", rrsig_signature = \# 128 38ba3da9bcd6e991e145513762047273a129a0ec758cf92a82eccb4b40c47ef29fd3ff732a524ad2d8c9ec2f9307361bce17a4a645d0d04ccc3b9c4f1cebce875617f244514099eb8df905c88055db15115802bfb6ee576b5b19c138385345ef856a6b7d60d94fa87dd963a8465eb495d4332f9d5b34e86dcd6e669d5714db99} 190tkls65e48jsa6g187pcns09nl34bn.iij.ad.jp. 3600(1 hour) IN NSEC3 RD_NSEC3 {nsec3_hashalg = SHA1, nsec3_flags = [OptOut], nsec3_iterations = 6, nsec3_salt = \# 8 318b14442ca75e0c, nsec3_next_hashed_owner_name = \# 20 0a950a2a95f82efd0de27be19b24931c58708869, nsec3_types = [A,RRSIG]} 190tkls65e48jsa6g187pcns09nl34bn.iij.ad.jp. 3600(1 hour) IN RRSIG RD_RRSIG {rrsig_type = NSEC3, rrsig_pubalg = RSASHA256, rrsig_num_labels = 4, rrsig_ttl = 3600(1 hour), rrsig_expiration = Sat, 20 Apr 2024 15:10:04 GMT, rrsig_inception = Thu, 21 Mar 2024 15:10:04 GMT, rrsig_key_tag = 54096, rrsig_zone = "iij.ad.jp.", rrsig_signature = \# 128 4be06c985800141741c4437490d424ae9b99a292e932cbc8d56900f9b1f81aabc1a27f2f45522fd93372bed389b2f8fa79ae3ac06fb27e24b4bf611dde60ab2736a08450379ae260298fd12cd8cf1b0ccbdbb082ec1fa7e2245c7e9e2c52cf56cd16893734db133900c68030b4fdaa3a66a3d787d2a8b3476cef117fb93b7dd6} 1j4c8550b3f7dn2ajpi06pjvgk57euvr.iij.ad.jp. 3600(1 hour) IN NSEC3 RD_NSEC3 {nsec3_hashalg = SHA1, nsec3_flags = [OptOut], nsec3_iterations = 6, nsec3_salt = \# 8 318b14442ca75e0c, nsec3_next_hashed_owner_name = \# 20 0d6b7306348b73e11121a9570430f5c71cb97ca8, nsec3_types = [A,AAAA,RRSIG]} 1j4c8550b3f7dn2ajpi06pjvgk57euvr.iij.ad.jp. 3600(1 hour) IN RRSIG RD_RRSIG {rrsig_type = NSEC3, rrsig_pubalg = RSASHA256, rrsig_num_labels = 4, rrsig_ttl = 3600(1 hour), rrsig_expiration = Sat, 20 Apr 2024 15:10:04 GMT, rrsig_inception = Thu, 21 Mar 2024 15:10:04 GMT, rrsig_key_tag = 54096, rrsig_zone = "iij.ad.jp.", rrsig_signature = \# 128 0993ea57fefe2de8770248909a90f8d3086eeb762ef36d418c7d6892914fee6f9703b2511b5fde166ea93e8d2b75ce6486039676559975d238f4334960378d75a5995b8e79ece34d80300d36b42667f3a13144771dbf947ab929cb9290d407b8eeeaf58791eb2756943620fb7126539abd80eb66255a59a0a6768df7eba69922} vljf5v5512cmafqsp572vc6fe842jiig.iij.ad.jp. 3600(1 hour) IN NSEC3 RD_NSEC3 {nsec3_hashalg = SHA1, nsec3_flags = [OptOut], nsec3_iterations = 6, nsec3_salt = \# 8 318b14442ca75e0c, nsec3_next_hashed_owner_name = \# 20 fde7cecf7e55b208bbeae0fe149cbc8686781189, nsec3_types = [A,NS,SOA,MX,TXT,AAAA,RRSIG,DNSKEY,NSEC3PARAM]} vljf5v5512cmafqsp572vc6fe842jiig.iij.ad.jp. 3600(1 hour) IN RRSIG RD_RRSIG {rrsig_type = NSEC3, rrsig_pubalg = RSASHA256, rrsig_num_labels = 4, rrsig_ttl = 3600(1 hour), rrsig_expiration = Sat, 20 Apr 2024 15:10:04 GMT, rrsig_inception = Thu, 21 Mar 2024 15:10:04 GMT, rrsig_key_tag = 54096, rrsig_zone = "iij.ad.jp.", rrsig_signature = \# 128 51515bdeeef0bdc88451e1827f23e874fd412a0bd6aa97e7a9a840cbbf4881481d664e96cea910917aff78d20f2f49d60a76035e7cbd3fcc846bdb4fe2e45adf621f029cd337d8bffebda94362247ef08dfeb07b29e2cb7bc0f02ef6281482696bf0f567ecf5ac9640d13a775fbf6c54ccdbf8197b6b8d62604d40e168bb5e4d} ;; ADDITIONAL SECTION:
- Canonical DNS Name Order https://datatracker.ietf.org/doc/html/rfc4034#section-6.1↩︎
- The Next Domain Name Field https://datatracker.ietf.org/doc/html/rfc4034#section-4.1.1↩︎
- Base 32 Encoding with Extended Hex Alphabet https://www.rfc-editor.org/rfc/rfc4648#section-7↩︎
- Base32Hex符号化前後でドメイン名正規順序が保たれます。↩︎
- “When using Opt-Out, names that are an insecure delegation (and empty non-terminals that are only derived from insecure delegations) don’t require an NSEC3 record.” https://datatracker.ietf.org/doc/html/rfc7129#section-5.1↩︎
- https://github.com/kazu-yamamoto/dnsext/tree/main/dnsext-bowline/bowline/↩︎
- https://github.com/kazu-yamamoto/dnsext/tree/main/dnsext-dnssec/↩︎
- https://github.com/kazu-yamamoto/dnsext/tree/main/dnsext-bowline/dug/。-i オプションで bowline と同様の反復検索を実行できます。↩︎