diff --git a/Libraries/LibDNS/CMakeLists.txt b/Libraries/LibDNS/CMakeLists.txt index ccb572a8e7b..710b22c7fa8 100644 --- a/Libraries/LibDNS/CMakeLists.txt +++ b/Libraries/LibDNS/CMakeLists.txt @@ -3,4 +3,4 @@ set(SOURCES ) serenity_lib(LibDNS dns) -target_link_libraries(LibDNS PRIVATE LibCore) +target_link_libraries(LibDNS PRIVATE LibCore PUBLIC LibCrypto) diff --git a/Libraries/LibDNS/Message.cpp b/Libraries/LibDNS/Message.cpp index d581bee1184..30fc6b89a4c 100644 --- a/Libraries/LibDNS/Message.cpp +++ b/Libraries/LibDNS/Message.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -662,11 +663,17 @@ ErrorOr DomainName::from_raw(ParseContext& ctx) constexpr static u8 OffsetMarkerMask = 0b11000000; if ((length & OffsetMarkerMask) == OffsetMarkerMask) { // This is a pointer to a prior domain name. - u16 const offset = static_cast(length & ~OffsetMarkerMask) << 8 | TRY(ctx.stream.read_value()); + u16 offset = static_cast(length & ~OffsetMarkerMask) << 8 | TRY(ctx.stream.read_value()); if (auto it = ctx.pointers->find_largest_not_above_iterator(offset); !it.is_end()) { auto labels = it->labels; - for (auto& entry : labels) - name.labels.append(entry); + size_t start_index = 0; + size_t start_entry_offset = offset - it.key(); + while (start_entry_offset > 0 && start_index < labels.size()) { + start_entry_offset -= labels[start_index].length() + 1; // +1 for the length byte + start_index++; + } + for (size_t i = start_index; i < labels.size(); ++i) + name.labels.append(labels[i].substring_view(i == start_index ? start_entry_offset : 0)); break; } dbgln("Invalid domain name pointer in label, no prior domain name found around offset {}", offset); @@ -702,6 +709,9 @@ ErrorOr DomainName::to_raw(ByteBuffer& out) const String DomainName::to_string() const { + if (labels.is_empty()) + return "."_string; + StringBuilder builder; for (size_t i = 0; i < labels.size(); ++i) { builder.append(labels[i]); @@ -711,6 +721,26 @@ String DomainName::to_string() const return MUST(builder.to_string()); } +String DomainName::to_canonical_string() const +{ + if (labels.is_empty()) + return "."_string; + + StringBuilder builder; + for (size_t i = 0; i < labels.size(); ++i) { + auto& label = labels[i]; + for (size_t j = 0; j < label.length(); ++j) { + auto ch = label[j]; + if (ch >= 'A' && ch <= 'Z') + ch = to_ascii_lowercase(ch); + builder.append(ch); + } + builder.append('.'); + } + + return MUST(builder.to_string()); +} + class RecordingStream final : public Stream { public: explicit RecordingStream(Stream& stream) @@ -762,9 +792,10 @@ ErrorOr ResourceRecord::from_raw(ParseContext& ctx) ResourceType type; Class class_; u32 ttl; + size_t original_offset = ctx.stream.read_bytes(); { RecordingStream rr_stream { ctx.stream }; - CountingStream rr_counting_stream { MaybeOwned(rr_stream) }; + CountingStream rr_counting_stream { MaybeOwned(rr_stream), original_offset }; ParseContext rr_ctx { rr_counting_stream, move(ctx.pointers) }; ScopeGuard guard([&] { ctx.pointers = move(rr_ctx.pointers); }); @@ -785,13 +816,14 @@ ErrorOr ResourceRecord::from_raw(ParseContext& ctx) class_ = static_cast(static_cast(TRY(rr_ctx.stream.read_value>()))); ttl = static_cast(TRY(rr_ctx.stream.read_value>())); auto rd_length = static_cast(TRY(rr_ctx.stream.read_value>())); + original_offset = rr_ctx.stream.read_bytes(); TRY(rr_ctx.stream.read_until_filled(TRY(rdata.get_bytes_for_writing(rd_length)))); rr_raw_data = move(rr_stream).take_recorded_data(); } FixedMemoryStream stream { rdata.bytes() }; - CountingStream rdata_stream { MaybeOwned(stream) }; + CountingStream rdata_stream { MaybeOwned(stream), original_offset }; ParseContext rdata_ctx { rdata_stream, move(ctx.pointers) }; ScopeGuard guard([&] { ctx.pointers = move(rdata_ctx.pointers); }); @@ -887,9 +919,11 @@ ErrorOr ResourceRecord::to_raw(ByteBuffer& buffer) const ErrorOr ResourceRecord::to_string() const { StringBuilder builder; + builder.appendff("[{} {} ", Messages::to_string(class_), Messages::to_string(type)); record.visit( [&](auto const& record) { builder.appendff("{}", MUST(record.to_string())); }, [&](ByteBuffer const& raw) { builder.appendff("{:hex-dump}", raw.bytes()); }); + builder.appendff(" | ttl={}, name={}]", ttl, name.to_string()); return builder.to_string(); } @@ -902,6 +936,15 @@ ErrorOr Records::A::from_raw(ParseContext& ctx) return Records::A { IPv4Address { address } }; } +ErrorOr Records::A::to_raw(ByteBuffer& buffer) const +{ + auto const address = this->address.to_u32(); + auto const net_address = bit_cast>(address); + auto bytes = TRY(buffer.get_bytes_for_writing(sizeof(net_address))); + bytes.overwrite(0, &net_address, sizeof(net_address)); + return {}; +} + ErrorOr Records::AAAA::from_raw(ParseContext& ctx) { // RFC 3596, 2.2. AAAA RDATA format. @@ -911,6 +954,18 @@ ErrorOr Records::AAAA::from_raw(ParseContext& ctx) return Records::AAAA { IPv6Address { bit_cast>(address) } }; } +ErrorOr Records::AAAA::to_raw(ByteBuffer& buffer) const +{ + auto const* const address_bytes = this->address.to_in6_addr_t(); + u128 address {}; + memcpy(&address, address_bytes, sizeof(address)); + + auto const net_address = bit_cast>(address); + auto bytes = TRY(buffer.get_bytes_for_writing(sizeof(net_address))); + bytes.overwrite(0, &net_address, sizeof(net_address)); + return {}; +} + ErrorOr Records::TXT::from_raw(ParseContext& ctx) { // RFC 1035, 3.3.14. TXT RDATA format. @@ -922,6 +977,18 @@ ErrorOr Records::TXT::from_raw(ParseContext& ctx) return Records::TXT { ByteString::copy(content) }; } +ErrorOr Records::TXT::to_raw(ByteBuffer& buffer) const +{ + auto const length = static_cast(content.length()); + auto length_bytes = TRY(buffer.get_bytes_for_writing(1)); + memcpy(length_bytes.data(), &length, 1); + + auto content_bytes = TRY(buffer.get_bytes_for_writing(length)); + memcpy(content_bytes.data(), content.characters(), length); + + return {}; +} + ErrorOr Records::CNAME::from_raw(ParseContext& ctx) { // RFC 1035, 3.3.1. CNAME RDATA format. @@ -931,6 +998,11 @@ ErrorOr Records::CNAME::from_raw(ParseContext& ctx) return Records::CNAME { move(name) }; } +ErrorOr Records::CNAME::to_raw(ByteBuffer& buffer) const +{ + return names.to_raw(buffer); +} + ErrorOr Records::NS::from_raw(ParseContext& ctx) { // RFC 1035, 3.3.11. NS RDATA format. @@ -962,6 +1034,23 @@ ErrorOr Records::SOA::from_raw(ParseContext& ctx) return Records::SOA { move(mname), move(rname), serial, refresh, retry, expire, minimum }; } +ErrorOr Records::SOA::to_raw(ByteBuffer& buffer) const +{ + TRY(mname.to_raw(buffer)); + TRY(rname.to_raw(buffer)); + + auto const output_size = 5 * sizeof(u32); + FixedMemoryStream stream { TRY(buffer.get_bytes_for_writing(output_size)) }; + + TRY(stream.write_value(static_cast>(serial))); + TRY(stream.write_value(static_cast>(refresh))); + TRY(stream.write_value(static_cast>(retry))); + TRY(stream.write_value(static_cast>(expire))); + TRY(stream.write_value(static_cast>(minimum))); + + return {}; +} + ErrorOr Records::MX::from_raw(ParseContext& ctx) { // RFC 1035, 3.3.9. MX RDATA format. @@ -1005,11 +1094,36 @@ ErrorOr Records::DNSKEY::from_raw(ParseContext& ctx) // | ALGORITHM| an 8-bit value that identifies the public key's cryptographic algorithm. // | PUBLICKEY| the public key material. + u32 key_tag = 0; auto flags = static_cast(TRY(ctx.stream.read_value>())); + key_tag += (bit_cast(NetworkOrdered(flags)) & 0xff) << 8; + key_tag += (bit_cast(NetworkOrdered(flags)) >> 8) & 0xff; auto protocol = TRY(ctx.stream.read_value()); + key_tag += static_cast(protocol) << 8; auto algorithm = static_cast(static_cast(TRY(ctx.stream.read_value()))); + key_tag += static_cast(algorithm); auto public_key = TRY(ctx.stream.read_until_eof()); - return Records::DNSKEY { flags, protocol, algorithm, move(public_key) }; + for (size_t i = 0; i < public_key.size(); ++i) { + key_tag += (i & 1) ? static_cast(public_key[i]) : static_cast(public_key[i]) << 8; + } + key_tag += (key_tag >> 16) & 0xffff; + + if (public_key.is_empty()) + return Error::from_string_literal("Empty public key in DNSKEY record"); + + return Records::DNSKEY { flags, protocol, algorithm, move(public_key), static_cast(key_tag & 0xffff) }; +} + +ErrorOr Records::DNSKEY::to_raw(ByteBuffer& buffer) const +{ + auto const output_size = 2 + 1 + 1 + public_key.size(); + FixedMemoryStream stream { TRY(buffer.get_bytes_for_writing(output_size)) }; + + TRY(stream.write_value(static_cast(bit_cast>(flags)))); + TRY(stream.write_value(protocol)); + TRY(stream.write_value(to_underlying(algorithm))); + TRY(stream.write_until_depleted(public_key.bytes())); + return {}; } ErrorOr Records::DS::from_raw(ParseContext& ctx) @@ -1051,6 +1165,19 @@ ErrorOr Records::DS::from_raw(ParseContext& ctx) return Records::DS { key_tag, algorithm, digest_type, move(digest) }; } +ErrorOr Records::DS::to_raw(ByteBuffer& buffer) const +{ + auto const output_size = 2 + 1 + 1 + digest.size(); + FixedMemoryStream stream { TRY(buffer.get_bytes_for_writing(output_size)) }; + + TRY(stream.write_value(static_cast>(key_tag))); + TRY(stream.write_value(static_cast(algorithm))); + TRY(stream.write_value(static_cast(digest_type))); + TRY(stream.write_until_depleted(digest.bytes())); + + return {}; +} + ErrorOr Records::SIG::from_raw(ParseContext& ctx) { // RFC 4034, 2.2. The SIG Resource Record. @@ -1077,6 +1204,29 @@ ErrorOr Records::SIG::from_raw(ParseContext& ctx) return Records::SIG { type_covered, algorithm, labels, original_ttl, UnixDateTime::from_seconds_since_epoch(signature_expiration), UnixDateTime::from_seconds_since_epoch(signature_inception), key_tag, move(signer_name), move(signature) }; } +ErrorOr Records::SIG::to_raw_excluding_signature(ByteBuffer& buffer) const +{ + AllocatingMemoryStream stream; + TRY(stream.write_value(static_cast>(to_underlying(type_covered)))); + TRY(stream.write_value(static_cast(algorithm))); + TRY(stream.write_value(label_count)); + TRY(stream.write_value(static_cast>(original_ttl))); + TRY(stream.write_value(static_cast>(expiration.seconds_since_epoch()))); + TRY(stream.write_value(static_cast>(inception.seconds_since_epoch()))); + TRY(stream.write_value(static_cast>(key_tag))); + + TRY(stream.read_until_filled(TRY(buffer.get_bytes_for_writing(stream.used_buffer_size())))); + TRY(signers_name.to_raw(buffer)); + return {}; +} + +ErrorOr Records::SIG::to_raw(ByteBuffer& buffer) const +{ + TRY(to_raw_excluding_signature(buffer)); + TRY(buffer.try_append(signature)); + return {}; +} + ErrorOr Records::SIG::to_string() const { // Single line: @@ -1110,6 +1260,18 @@ ErrorOr Records::HINFO::from_raw(ParseContext& ctx) return Records::HINFO { ByteString::copy(cpu), ByteString::copy(os) }; } +ErrorOr Records::HINFO::to_raw(ByteBuffer& buffer) const +{ + auto allocated_length = cpu.length() + os.length() + 2; + auto bytes = TRY(buffer.get_bytes_for_writing(allocated_length)); + FixedMemoryStream stream { bytes }; + TRY(stream.write_value(static_cast(cpu.length()))); + TRY(stream.write_until_depleted(cpu.bytes())); + TRY(stream.write_value(static_cast(os.length()))); + TRY(stream.write_until_depleted(os.bytes())); + return {}; +} + ErrorOr Records::OPT::from_raw(ParseContext& ctx) { // RFC 6891, 6.1. The OPT pseudo-RR. diff --git a/Libraries/LibDNS/Message.h b/Libraries/LibDNS/Message.h index 58274d4d664..0f23fefe4b6 100644 --- a/Libraries/LibDNS/Message.h +++ b/Libraries/LibDNS/Message.h @@ -91,6 +91,16 @@ struct DomainName { static ErrorOr from_raw(ParseContext&); ErrorOr to_raw(ByteBuffer&) const; String to_string() const; + String to_canonical_string() const; + DomainName parent() const + { + auto copy = *this; + copy.labels.take_first(); + return copy; + } + + bool operator==(DomainName const&) const& = default; + bool operator!=(DomainName const&) const& = default; }; // Listing from IANA https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4. @@ -278,6 +288,8 @@ static inline StringView to_string(Algorithm algorithm) return "ED25519"sv; case Algorithm::Unknown: return "Unknown"sv; + default: + return "Invalid"sv; } VERIFY_NOT_REACHED(); } @@ -364,7 +376,7 @@ struct A { static constexpr ResourceType type = ResourceType::A; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return address.to_string(); } }; struct AAAA { @@ -372,7 +384,7 @@ struct AAAA { static constexpr ResourceType type = ResourceType::AAAA; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return address.to_string(); } }; struct TXT { @@ -380,7 +392,7 @@ struct TXT { static constexpr ResourceType type = ResourceType::TXT; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return String::formatted("Text: '{}'", StringView { content }); } }; struct CNAME { @@ -388,7 +400,7 @@ struct CNAME { static constexpr ResourceType type = ResourceType::CNAME; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return names.to_string(); } }; struct NS { @@ -396,7 +408,7 @@ struct NS { static constexpr ResourceType type = ResourceType::NS; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: NS::to_raw"); } ErrorOr to_string() const { return name.to_string(); } }; struct SOA { @@ -410,7 +422,7 @@ struct SOA { static constexpr ResourceType type = ResourceType::SOA; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return String::formatted("SOA MName: '{}', RName: '{}', Serial: {}, Refresh: {}, Retry: {}, Expire: {}, Minimum: {}", mname.to_string(), rname.to_string(), serial, refresh, retry, expire, minimum); @@ -422,7 +434,7 @@ struct MX { static constexpr ResourceType type = ResourceType::MX; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: MX::to_raw"); } ErrorOr to_string() const { return String::formatted("MX Preference: {}, Exchange: '{}'", preference, exchange.to_string()); } }; struct PTR { @@ -430,7 +442,7 @@ struct PTR { static constexpr ResourceType type = ResourceType::PTR; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: PTR::to_raw"); } ErrorOr to_string() const { return name.to_string(); } }; struct SRV { @@ -441,7 +453,7 @@ struct SRV { static constexpr ResourceType type = ResourceType::SRV; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: SRV::to_raw"); } ErrorOr to_string() const { return String::formatted("SRV Priority: {}, Weight: {}, Port: {}, Target: '{}'", priority, weight, port, target.to_string()); } }; struct DNSKEY { @@ -449,6 +461,17 @@ struct DNSKEY { u8 protocol; DNSSEC::Algorithm algorithm; ByteBuffer public_key; + // Extra: calculated key tag + u16 calculated_key_tag; + // Extra: public key components (pointing into public_key) ONLY for RSA. + u16 public_key_rsa_exponent_length() const + { + if (public_key[0] != 0) + return public_key[0]; + return static_cast(public_key[1]) | static_cast(public_key[2]) << 8; + } + ReadonlyBytes public_key_rsa_exponent() const { return public_key.bytes().slice(1, public_key_rsa_exponent_length()); } + ReadonlyBytes public_key_rsa_modulus() const { return public_key.bytes().slice(1 + public_key_rsa_exponent_length()); } constexpr static inline u16 FlagSecureEntryPoint = 0b1000000000000000; constexpr static inline u16 FlagZoneKey = 0b0100000000000000; @@ -461,10 +484,10 @@ struct DNSKEY { static constexpr ResourceType type = ResourceType::DNSKEY; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { - return String::formatted("DNSKEY Flags: {}{}{}{}({}), Protocol: {}, Algorithm: {}, Public Key: {}", + return String::formatted("DNSKEY Flags: {}{}{}{}({}), Protocol: {}, Algorithm: {}, Public Key: {}, Tag: {}", is_secure_entry_point() ? "sep "sv : ""sv, is_zone_key() ? "zone "sv : ""sv, is_revoked() ? "revoked "sv : ""sv, @@ -472,7 +495,8 @@ struct DNSKEY { flags, protocol, DNSSEC::to_string(algorithm), - TRY(encode_base64(public_key))); + TRY(encode_base64(public_key)), + calculated_key_tag); } }; struct CDNSKEY : public DNSKEY { @@ -493,8 +517,15 @@ struct DS { static constexpr ResourceType type = ResourceType::DS; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } - ErrorOr to_string() const { return "DS"_string; } + ErrorOr to_raw(ByteBuffer&) const; + ErrorOr to_string() const + { + return String::formatted("DS Key Tag: {}, Algorithm: {}, Digest Type: {}, Digest: {}", + key_tag, + DNSSEC::to_string(algorithm), + DNSSEC::to_string(digest_type), + TRY(encode_base64(digest))); + } }; struct CDS : public DS { template @@ -518,7 +549,8 @@ struct SIG { static constexpr ResourceType type = ResourceType::SIG; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; + ErrorOr to_raw_excluding_signature(ByteBuffer&) const; ErrorOr to_string() const; }; struct RRSIG : public SIG { @@ -530,6 +562,7 @@ struct RRSIG : public SIG { static constexpr ResourceType type = ResourceType::RRSIG; static ErrorOr from_raw(ParseContext& raw) { return SIG::from_raw(raw); } + ErrorOr to_raw_excluding_signature(ByteBuffer& buffer) const { return SIG::to_raw_excluding_signature(buffer); } }; struct NSEC { DomainName next_domain_name; @@ -537,7 +570,7 @@ struct NSEC { static constexpr ResourceType type = ResourceType::NSEC; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: NSC::to_raw"); } ErrorOr to_string() const { return "NSEC"_string; } }; struct NSEC3 { @@ -550,7 +583,7 @@ struct NSEC3 { static constexpr ResourceType type = ResourceType::NSEC3; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: NSEC3::to_raw"); } ErrorOr to_string() const { return "NSEC3"_string; } }; struct NSEC3PARAM { @@ -565,7 +598,7 @@ struct NSEC3PARAM { static constexpr ResourceType type = ResourceType::NSEC3PARAM; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: NSEC3PARAM::to_raw"); } ErrorOr to_string() const { return "NSEC3PARAM"_string; } }; struct TLSA { @@ -575,7 +608,7 @@ struct TLSA { ByteBuffer certificate_association_data; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented: TLSA::to_raw"); } ErrorOr to_string() const { return "TLSA"_string; } }; struct HINFO { @@ -584,7 +617,7 @@ struct HINFO { static constexpr ResourceType type = ResourceType::HINFO; static ErrorOr from_raw(ParseContext&); - ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_raw(ByteBuffer&) const; ErrorOr to_string() const { return String::formatted("HINFO CPU: '{}', OS: '{}'", StringView { cpu }, StringView { os }); } }; struct OPT { diff --git a/Libraries/LibDNS/Resolver.h b/Libraries/LibDNS/Resolver.h index 46fd98303d0..00884d8fc26 100644 --- a/Libraries/LibDNS/Resolver.h +++ b/Libraries/LibDNS/Resolver.h @@ -7,9 +7,11 @@ #pragma once #include +#include #include #include #include +#include #include #include #include @@ -17,12 +19,43 @@ #include #include #include +#include +#include +#include #include -#include #include +#define TRY_OR_REJECT_PROMISE(promise, expr) \ + ({ \ + auto _result = (expr); \ + if (_result.is_error()) { \ + promise->reject(_result.release_error()); \ + return promise; \ + } \ + _result.release_value(); \ + }) + namespace DNS { +// FIXME: Load these keys from a file (likely something trusted by the system, e.g. "whatever systemd does"). +// https://data.iana.org/root-anchors/root-anchors.xml +static Vector s_root_zone_dnskeys = { + { + .flags = 257, + .protocol = 3, + .algorithm = Messages::DNSSEC::Algorithm::RSASHA256, + .public_key = decode_base64("AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU="sv).release_value(), + .calculated_key_tag = 20326, + }, + { + .flags = 256, + .protocol = 3, + .algorithm = Messages::DNSSEC::Algorithm::RSASHA256, + .public_key = decode_base64("AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/cidltpkyGwCJNnOAlFNKF2jBosZBU5eeHspaQWOmOElZsjICMQMC3aeHbGiShvZsx4wMYSjH8e7Vrhbu6irwCzVBApESjbUdpWWmEnhathWu1jo+siFUiRAAxm9qyJNg/wOZqqzL/dL/q8PkcRU5oUKEpUge71M3ej2/7CPqpdVwuMoTvoB+ZOT4YeGyxMvHmbrxlFzGOHOijtzN+u1TQNatX2XBuzZNQ1K+s2CXkPIZo7s6JgZyvaBevYtxPvYLw4z9mR7K2vaF18UYH9Z9GNUUeayffKC73PYc="sv).release_value(), + .calculated_key_tag = 38696, + }, +}; + class Resolver; class LookupResult : public AtomicRefCounted @@ -57,7 +90,8 @@ public: dbgln_if(DNS_DEBUG, "DNS: Removing expired record for {}", m_name.to_string()); m_cached_records.remove(i); } else { - dbgln_if(DNS_DEBUG, "DNS: Keeping record for {} (expires in {})", m_name.to_string(), record.expiration.has_value() ? record.expiration.value().to_string() : "never"_string); + dbgln_if(DNS_DEBUG, "DNS: Keeping record for {} (expires in {})", m_name.to_string(), + record.expiration.has_value() ? record.expiration.value().to_string() : "never"_string); ++i; } } @@ -81,6 +115,35 @@ public: return result; } + Vector records(Messages::ResourceType type) const + { + Vector result; + for (auto& re : m_cached_records) { + if (re.record.type == type) + result.append(re.record); + } + return result; + } + + Messages::ResourceRecord const& record(Messages::ResourceType type) const + { + for (auto const& re : m_cached_records) { + if (re.record.type == type) + return re.record; + } + VERIFY_NOT_REACHED(); + } + + template + RR const& record() const + { + for (auto const& re : m_cached_records) { + if (re.record.type == RR::type) + return re.record.record.get(); + } + VERIFY_NOT_REACHED(); + } + bool has_record_of_type(Messages::ResourceType type, bool later = false) const { if (later && m_desired_types.contains(type)) @@ -101,18 +164,35 @@ public: bool can_be_removed() const { return !m_valid && m_request_done; } bool is_done() const { return m_request_done; } + void set_dnssec_validated(bool validated) { m_dnssec_validated = validated; } + bool is_dnssec_validated() const { return m_dnssec_validated; } + void set_being_dnssec_validated(bool validated) { m_being_dnssec_validated = validated; } + bool is_being_dnssec_validated() const { return m_being_dnssec_validated; } Messages::DomainName const& name() const { return m_name; } + Vector const& used_dnskeys() const { return m_used_dnskeys; } + void add_dnskey(Messages::Records::DNSKEY key) + { + if (m_seen_key_tags.set(key.calculated_key_tag) == AK::HashSetResult::InsertedNewEntry) + m_used_dnskeys.append(move(key)); + } + private: bool m_valid { false }; bool m_request_done { false }; + bool m_dnssec_validated { false }; + bool m_being_dnssec_validated { false }; Messages::DomainName m_name; + struct RecordWithExpiration { Messages::ResourceRecord record; Optional expiration; }; + Vector m_cached_records; HashTable m_desired_types; + Vector m_used_dnskeys {}; + HashTable m_seen_key_tags; u16 m_id { 0 }; }; @@ -120,6 +200,7 @@ class Resolver { struct PendingLookup { u16 id { 0 }; ByteString name; + Messages::DomainName parsed_name; WeakPtr result; NonnullRefPtr>> promise; NonnullRefPtr repeat_timer; @@ -132,6 +213,13 @@ public: UDP, }; + struct LookupOptions { + bool validate_dnssec_locally { false }; + PendingLookup* repeating_lookup { nullptr }; + + static LookupOptions default_() { return {}; } + }; + struct SocketResult { MaybeOwned socket; ConnectionMode mode; @@ -213,25 +301,29 @@ public: }); } - NonnullRefPtr>> lookup(ByteString name, Messages::Class class_ = Messages::Class::IN) + NonnullRefPtr>> lookup(ByteString name, Messages::Class class_ = Messages::Class::IN, LookupOptions options = LookupOptions::default_()) { - return lookup(move(name), class_, { Messages::ResourceType::A, Messages::ResourceType::AAAA }); + return lookup(move(name), class_, { Messages::ResourceType::A, Messages::ResourceType::AAAA }, options); } - NonnullRefPtr>> lookup(ByteString name, Messages::Class class_, Vector desired_types, PendingLookup* repeating_lookup = nullptr) + NonnullRefPtr>> lookup(ByteString name, Messages::Class class_, Vector desired_types, LookupOptions options = LookupOptions::default_()) { flush_cache(); - if (repeating_lookup && repeating_lookup->times_repeated >= 5) { - auto promise = repeating_lookup->promise; + if (options.repeating_lookup && options.repeating_lookup->times_repeated >= 5) { + dbgln_if(DNS_DEBUG, "DNS: Repeating lookup for {} timed out", name); + auto promise = options.repeating_lookup->promise; promise->reject(Error::from_string_literal("DNS lookup timed out")); - m_pending_lookups.with_write_locked([&](auto& lookups) { lookups->remove(repeating_lookup->id); }); + m_pending_lookups.with_write_locked([&](auto& lookups) { + lookups->remove(options.repeating_lookup->id); + }); return promise; } - auto promise = repeating_lookup ? repeating_lookup->promise : Core::Promise>::construct(); + auto promise = options.repeating_lookup ? options.repeating_lookup->promise : Core::Promise>::construct(); if (auto maybe_ipv4 = IPv4Address::from_string(name); maybe_ipv4.has_value()) { + dbgln_if(DNS_DEBUG, "DNS: Resolving {} as IPv4", name); if (desired_types.contains_slow(Messages::ResourceType::A)) { auto result = make_ref_counted(Messages::DomainName {}); result->add_record({ .name = {}, .type = Messages::ResourceType::A, .class_ = Messages::Class::IN, .ttl = 0, .record = Messages::Records::A { maybe_ipv4.release_value() }, .raw = {} }); @@ -242,6 +334,7 @@ public: } if (auto maybe_ipv6 = IPv6Address::from_string(name); maybe_ipv6.has_value()) { + dbgln_if(DNS_DEBUG, "DNS: Resolving {} as IPv6", name); if (desired_types.contains_slow(Messages::ResourceType::AAAA)) { auto result = make_ref_counted(Messages::DomainName {}); result->add_record({ .name = {}, .type = Messages::ResourceType::AAAA, .class_ = Messages::Class::IN, .ttl = 0, .record = Messages::Records::AAAA { maybe_ipv6.release_value() }, .raw = {} }); @@ -252,13 +345,23 @@ public: } if (auto result = lookup_in_cache(name, class_, desired_types)) { - promise->resolve(result.release_nonnull()); - return promise; + dbgln_if(DNS_DEBUG, "DNS: Resolving {} from cache...", name); + if (!options.validate_dnssec_locally || result->is_dnssec_validated()) { + dbgln_if(DNS_DEBUG, "DNS: Resolved {} from cache", name); + promise->resolve(result.release_nonnull()); + return promise; + } + dbgln_if(DNS_DEBUG, "DNS: Cache entry for {} is not DNSSEC validated (and we expect that), re-resolving", name); } auto domain_name = Messages::DomainName::from_string(name); if (!has_connection()) { + if (options.validate_dnssec_locally) { + promise->reject(Error::from_string_literal("No connection available to validate DNSSEC")); + return promise; + } + // Use system resolver // FIXME: Use an underlying resolver instead. dbgln_if(DNS_DEBUG, "Not ready to resolve, using system resolver and skipping cache for {}", name); @@ -286,27 +389,38 @@ public: auto already_in_cache = false; auto result = m_cache.with_write_locked([&](auto& cache) -> NonnullRefPtr { + dbgln_if(DNS_DEBUG, "DNS: Resolving {}...", name); auto existing = [&] -> RefPtr { if (cache.contains(name)) { + dbgln_if(DNS_DEBUG, "DNS: Resolving {} from cache...", name); auto ptr = *cache.get(name); - already_in_cache = true; + already_in_cache = (!options.validate_dnssec_locally && !ptr->is_being_dnssec_validated()) || ptr->is_dnssec_validated(); for (auto const& type : desired_types) { - if (!ptr->has_record_of_type(type, true)) { + if (!ptr->has_record_of_type(type, !options.validate_dnssec_locally && !ptr->is_being_dnssec_validated())) { already_in_cache = false; break; } } + dbgln_if(DNS_DEBUG, "DNS: Found {} in cache, already_in_cache={}", name, already_in_cache); + dbgln_if(DNS_DEBUG, "DNS: That entry is {} DNSSEC validated", ptr->is_dnssec_validated() ? "already" : "not"); + for (auto const& entry : ptr->records()) + dbgln_if(DNS_DEBUG, "DNS: Found record of type {}", Messages::to_string(entry.type)); return ptr; } return nullptr; }(); - if (existing) + if (existing) { + dbgln_if(DNS_DEBUG, "DNS: Resolved {} from cache", name); return *existing; + } + dbgln_if(DNS_DEBUG, "DNS: Adding {} to cache", name); auto ptr = make_ref_counted(domain_name); + if (!ptr->is_dnssec_validated()) + ptr->set_dnssec_validated(options.validate_dnssec_locally); for (auto const& type : desired_types) ptr->will_add_record_of_type(type); cache.set(name, ptr); @@ -317,11 +431,12 @@ public: if (already_in_cache) { auto id = result->id(); cached_result_id = id; - auto existing_promise = m_pending_lookups.with_write_locked([&](auto& lookups) -> RefPtr>> { - if (auto* lookup = lookups->find(id)) - return lookup->promise; - return nullptr; - }); + auto existing_promise = m_pending_lookups.with_write_locked( + [&](auto& lookups) -> RefPtr>> { + if (auto* lookup = lookups->find(id)) + return lookup->promise; + return nullptr; + }); if (existing_promise) return existing_promise.release_nonnull(); @@ -334,9 +449,9 @@ public: } Messages::Message query; - if (repeating_lookup) { - query.header.id = repeating_lookup->id; - repeating_lookup->times_repeated++; + if (options.repeating_lookup) { + query.header.id = options.repeating_lookup->id; + options.repeating_lookup->times_repeated++; } else { m_pending_lookups.with_read_locked([&](auto& lookups) { do @@ -364,23 +479,48 @@ public: }); } - auto cached_entry = repeating_lookup ? nullptr : m_pending_lookups.with_write_locked([&](auto& pending_lookups) -> PendingLookup* { - // One more try to make sure we're not overwriting an existing lookup - if (cached_result_id.has_value()) { - if (auto* lookup = pending_lookups->find(*cached_result_id)) - return lookup; - } - - pending_lookups->insert(query.header.id, { query.header.id, name, result->make_weak_ptr(), promise, Core::Timer::create(), 0 }); - auto p = pending_lookups->find(query.header.id); - p->repeat_timer->set_single_shot(true); - p->repeat_timer->set_interval(1000); - p->repeat_timer->on_timeout = [=, this] { - (void)lookup(name, class_, desired_types, p); + if (options.validate_dnssec_locally) { + query.header.additional_count = 1; + query.header.options.set_checking_disabled(true); + query.header.options.set_authenticated_data(true); + auto opt = Messages::Records::OPT { + .udp_payload_size = 4096, + .extended_rcode_and_flags = 0, + .options = {}, }; + opt.set_dnssec_ok(true); - return nullptr; - }); + query.additional_records.append(Messages::ResourceRecord { + .name = Messages::DomainName::from_string(""sv), + .type = Messages::ResourceType::OPT, + .class_ = class_, + .ttl = 0, + .record = move(opt), + .raw = {}, + }); + } + + result->set_id(query.header.id); + + auto cached_entry = options.repeating_lookup + ? nullptr + : m_pending_lookups.with_write_locked([&](auto& pending_lookups) -> PendingLookup* { + // One more try to make sure we're not overwriting an existing lookup + if (cached_result_id.has_value()) { + if (auto* lookup = pending_lookups->find(*cached_result_id)) + return lookup; + } + + pending_lookups->insert(query.header.id, { query.header.id, name, domain_name, result->make_weak_ptr(), promise, Core::Timer::create(), 0 }); + auto p = pending_lookups->find(query.header.id); + p->repeat_timer->set_single_shot(true); + p->repeat_timer->set_interval(1000); + p->repeat_timer->on_timeout = [=, this] { + (void)lookup(name, class_, desired_types, { .validate_dnssec_locally = options.validate_dnssec_locally, .repeating_lookup = p }); + }; + + return nullptr; + }); if (cached_entry) { dbgln_if(DNS_DEBUG, "DNS::lookup({}) -> Lookup already underway", name); @@ -447,7 +587,10 @@ private: void process_incoming_messages() { while (true) { - if (auto result = m_socket.with_read_locked([](auto& socket) { return (*socket)->can_read_without_blocking(); }); result.is_error() || !result.value()) + if (auto result = m_socket.with_read_locked([](auto& socket) { + return (*socket)->can_read_without_blocking(); + }); + result.is_error() || !result.value()) break; auto message_or_err = parse_one_message(); if (message_or_err.is_error()) { @@ -462,12 +605,17 @@ private: if (!lookup) return Error::from_string_literal("No pending lookup found for this message"); - if (lookup->result.is_null()) + if (lookup->result.is_null()) { + dbgln_if(DNS_DEBUG, "DNS: Received a message with no pending lookup (id={})", message.header.id); return {}; // Message is a response to a lookup that's been purged from the cache, ignore it + } lookup->repeat_timer->stop(); auto result = lookup->result.strong_ref(); + if (result->is_dnssec_validated()) + return validate_dnssec(move(message), *lookup, *result); + for (auto& record : message.answers) result->add_record(move(record)); @@ -476,11 +624,475 @@ private: lookups->remove(message.header.id); return {}; }); - if (result.is_error()) { + if (result.is_error()) dbgln_if(DNS_DEBUG, "DNS: Received a message with no pending lookup: {}", result.error()); - continue; + } + } + + using RRSet = Vector; + struct CanonicalizedRRSetWithRRSIG { + RRSet rrset; + Messages::Records::RRSIG rrsig; + Vector dnskeys; + }; + + // https://www.rfc-editor.org/rfc/rfc2535 + NonnullRefPtr> validate_dnssec_chain_step(Messages::DomainName const& name, bool top_level = false) + { + dbgln_if(DNS_DEBUG, "DNS: Validating DNSSEC chain for {}", name.to_string()); + auto promise = Core::Promise::construct(); + // 6.3.1. authentication leads to chains of alternating SIG and KEY RRs with the first SIG + // signing the original data whose authenticity is to be shown and the final KEY + // being some trusted key staticly configured at the resolver performing + // the authentication. + // If this is the root, we're done, just return true. + if (name.labels.size() == 0) { + promise->resolve(true); + return promise; + } + + // 2.3. Every name in a secured zone will have associated with it at least + // one SIG resource record for each resource type under that name except + // for glue address RRs and delegation point NS RRs. A security aware + // server will attempt to return, with RRs retrieved, the corresponding + // SIGs. If a server is not security aware, the resolver must retrieve + // all the SIG records for a name and select the one or ones that sign + // the resource record set(s) that resolver is interested in. + // + // 2.3.4 There MUST be a zone KEY RR, signed by its superzone, for every + // subzone if the superzone is secure. This will normally appear in the + // subzone and may also be included in the superzone. But, in the case + // of an unsecured subzone which can not or will not be modified to add + // any security RRs, a KEY declaring the subzone to be unsecured MUST + // appear with the superzone signature in the superzone, if the + // superzone is secure. For all but one other RR type the data from the + // subzone is more authoritative so only the subzone KEY RR should be + // signed in the superzone if it appears there. The NS and any glue + // address RRs SHOULD only be signed in the subzone. The SOA and any + // other RRs that have the zone name as owner should appear only in the + // subzone and thus are signed only there. + + // Figure out if this is a delegation point (this should really be optimised to avoid sequential lookups of SOA -> DS -> NS for "just" the same zone). + // - Lookup the SOA record for the domain. + auto soa_result = TRY_OR_REJECT_PROMISE(promise, (lookup(name.to_string().to_byte_string(), Messages::Class::IN, { Messages::ResourceType::SOA }, { .validate_dnssec_locally = !top_level })->await())); + // - If we have no SOA record- + if (!soa_result->has_record_of_type(Messages::ResourceType::SOA)) { + dbgln_if(DNS_DEBUG, "DNS: No SOA record found for {}", name.to_string()); + // - First, check for a DS record- + auto ds_result = TRY_OR_REJECT_PROMISE(promise, (lookup(name.to_string().to_byte_string(), Messages::Class::IN, { Messages::ResourceType::DS }, { .validate_dnssec_locally = !top_level })->await())); + // - If there's no DS record, check for an NS record- + if (!ds_result->has_record_of_type(Messages::ResourceType::DS)) { + dbgln_if(DNS_DEBUG, "DNS: No DS record found for {}", name.to_string()); + // - If there's no DS record, check for an NS record- + auto ns_result = TRY_OR_REJECT_PROMISE(promise, (lookup(name.to_string().to_byte_string(), Messages::Class::IN, { Messages::ResourceType::NS }, { .validate_dnssec_locally = !top_level })->await())); + if (ns_result->has_record_of_type(Messages::ResourceType::NS)) { + // - but if there _is_ an NS record, this is a broken delegation, so reject. + dbgln_if(DNS_DEBUG, "DNS: Found NS record for {}", name.to_string()); + promise->resolve(false); + return promise; + } + dbgln_if(DNS_DEBUG, "DNS: No NS record found for {}", name.to_string()); + // this is just part of the parent delegation, so go up one level. + return validate_dnssec_chain_step(name.parent()); + } + // - If there is a DS record, this is a separate zone...but since we don't have an SOA record, this is a misconfigured zone. + // Let's just reject. + dbgln_if(DNS_DEBUG, "DNS: Found DS record for {}", name.to_string()); + promise->resolve(false); + return promise; + } + + // So we have an SOA record, there's much rejoicing and we can continue. + auto& soa = soa_result->record(); + dbgln_if(DNS_DEBUG, "DNS: Found SOA record for {}: {}", name.to_string(), soa.mname.to_string()); + if (soa.mname == name.parent()) { + // Just go up one level, all is well. + return validate_dnssec_chain_step(name.parent()); + } + + // This is a separate zone, let's look up the DS record. + auto ds_result = TRY_OR_REJECT_PROMISE(promise, (lookup(name.to_string().to_byte_string(), Messages::Class::IN, { Messages::ResourceType::DS }, { .validate_dnssec_locally = false })->await())); + if (!ds_result->has_record_of_type(Messages::ResourceType::DS)) { + // If there's no DS record, this is a misconfigured zone. + dbgln_if(DNS_DEBUG, "DNS: No DS record found for {}", name.to_string()); + promise->resolve(false); + return promise; + } + + promise->resolve(true); + return promise; + } + + ErrorOr validate_dnssec(Messages::Message message, PendingLookup& lookup, NonnullRefPtr result) + { + struct RecordAndRRSIG { + Vector records; + Messages::Records::RRSIG rrsig; + }; + HashMap records_with_rrsigs; + for (auto& record : message.answers) { + if (record.type == Messages::ResourceType::RRSIG) { + auto& rrsig = record.record.get(); + auto type = rrsig.type_covered; + if (auto found = records_with_rrsigs.get(type); found.has_value()) + found->rrsig = move(rrsig); + else + records_with_rrsigs.set(type, { {}, move(rrsig) }); + } else { + auto type = record.type; + if (auto found = records_with_rrsigs.get(record.type); found.has_value()) + found->records.append(move(record)); + else + records_with_rrsigs.set(type, { { move(record) }, {} }); } } + + if (records_with_rrsigs.is_empty()) { + dbgln_if(DNS_DEBUG, "DNS: No RRSIG records found in DNSSEC response"); + return {}; + } + + auto name = result->name(); + + Core::deferred_invoke([this, lookup, name, records_with_rrsigs = move(records_with_rrsigs), result = move(result)] mutable { + dbgln_if(DNS_DEBUG, "DNS: Resolving DNSKEY for {}", name.to_string()); + result->set_dnssec_validated(false); // Will be set to true if we successfully validate the RRSIGs. + result->set_being_dnssec_validated(true); + + Vector parent_zone_keys; + auto is_root_zone = lookup.parsed_name.labels.size() == 0; + + if (!is_root_zone) { + auto chain_valid_result = validate_dnssec_chain_step(name, true)->await(); + if (chain_valid_result.is_error()) { + lookup.promise->reject(chain_valid_result.release_error()); + return; + } + if (!chain_valid_result.value()) { + lookup.promise->reject(Error::from_string_literal("DNSSEC chain is invalid")); + return; + } + auto parent_result = this->lookup(lookup.parsed_name.parent().to_string().to_byte_string(), Messages::Class::IN, { Messages::ResourceType::DNSKEY }, { .validate_dnssec_locally = true }) + ->await(); + if (parent_result.is_error()) { + lookup.promise->reject(parent_result.release_error()); + return; + } + + if (!parent_result.value()->is_dnssec_validated()) { + lookup.promise->reject(Error::from_string_literal("Parent zone is not DNSSEC validated")); + return; + } + + parent_zone_keys = parent_result.value()->used_dnskeys(); + for (auto& rr : parent_result.value()->records(Messages::ResourceType::DNSKEY)) + parent_zone_keys.append(rr.record.get()); + dbgln("Found {} DNSKEYs for parent zone ({})", parent_zone_keys.size(), lookup.parsed_name.parent().to_string()); + } + + auto resolve_using_keys = [=, this, records_with_rrsigs = move(records_with_rrsigs)](Vector keys) mutable { + dbgln_if(DNS_DEBUG, "DNS: Validating {} RRSIGs for {}; starting with {} keys", records_with_rrsigs.size(), name.to_string(), keys.size()); + for (auto& key : keys) + dbgln_if(DNS_DEBUG, "- DNSKEY: {}", key.to_string()); + Vector>> promises; + + for (auto& record_and_rrsig : records_with_rrsigs) { + auto& records = record_and_rrsig.value.records; + if (record_and_rrsig.key == Messages::ResourceType::DNSKEY) { + for (auto& record : records) + keys.append(record.record.get()); + } + } + + dbgln_if(DNS_DEBUG, "DNS: Found {} keys total", keys.size()); + + // (owner | type | class) -> (RRSet, RRSIG, DNSKey*) + HashMap rrsets_with_rrsigs; + + for (auto& [type, pair] : records_with_rrsigs) { + auto& records = pair.records; + auto& rrsig = pair.rrsig; + + for (auto& record : records) { + auto canonicalized_name = record.name.to_canonical_string(); + auto key = MUST(String::formatted("{}|{}|{}", canonicalized_name, to_underlying(record.type), to_underlying(record.class_))); + + if (!rrsets_with_rrsigs.contains(key)) { + auto dnskeys = [&] -> Vector { + Vector relevant_keys; + for (auto& key : keys) { + if (key.algorithm == rrsig.algorithm) + relevant_keys.append(key); + } + return relevant_keys; + }(); + dbgln_if(DNS_DEBUG, "DNS: Found {} relevant DNSKEYs for key {}", dnskeys.size(), key); + rrsets_with_rrsigs.set(key, CanonicalizedRRSetWithRRSIG { {}, move(rrsig), move(dnskeys) }); + } + auto& rrset_with_rrsig = *rrsets_with_rrsigs.get(key); + rrset_with_rrsig.rrset.append(move(record)); + } + } + + for (auto& entry : rrsets_with_rrsigs) { + auto& rrset_with_rrsig = entry.value; + + if (rrset_with_rrsig.dnskeys.is_empty()) { + dbgln_if(DNS_DEBUG, "DNS: No DNSKEY found for validation of {} RRs", rrset_with_rrsig.rrset.size()); + continue; + } + + promises.append(validate_rrset_with_rrsig(move(rrset_with_rrsig), result)); + } + + auto promise = Core::Promise::after(move(promises)) + ->when_resolved([result, lookup, keys = move(keys)](Empty) { + for (auto& key : keys) + result->add_dnskey(key); + result->set_dnssec_validated(true); + result->set_being_dnssec_validated(false); + result->finished_request(); + lookup.promise->resolve(result); + }) + .when_rejected([result, lookup](Error& error) { + result->finished_request(); + result->set_being_dnssec_validated(false); + lookup.promise->reject(move(error)); + }) + .map>([result](Empty&) { return result; }); + + lookup.promise = move(promise); + }; + + if (is_root_zone) { + resolve_using_keys(s_root_zone_dnskeys); + return; + } + + dbgln_if(DNS_DEBUG, "DNS: Starting DNSKEY lookup for {}", lookup.name); + this->lookup(lookup.name, Messages::Class::IN, { Messages::ResourceType::DNSKEY }, { .validate_dnssec_locally = false }) + ->when_resolved([=](NonnullRefPtr& dnskey_lookup_result) mutable { + dbgln_if(DNS_DEBUG, "DNSKEY for {}:", name.to_string()); + auto key_records = dnskey_lookup_result->records(Messages::ResourceType::DNSKEY); + for (auto& record : key_records) + dbgln_if(DNS_DEBUG, "- DNSKEY: {}", record.to_string()); + Vector keys; + keys.ensure_capacity(parent_zone_keys.size() + dnskey_lookup_result->records().size()); + for (auto& record : parent_zone_keys) + keys.append(record); + for (auto& record : key_records) + keys.append(move(record.record).get()); + resolve_using_keys(move(keys)); + }) + .when_rejected([=](auto& error) mutable { + if (parent_zone_keys.is_empty()) { + dbgln_if(DNS_DEBUG, "Failed to resolve DNSKEY for {}: {}", name.to_string(), error); + lookup.promise->reject(move(error)); + } + resolve_using_keys(move(parent_zone_keys)); + }); + }); + + return {}; + } + + Messages::Records::DNSKEY const* find_dnskey(CanonicalizedRRSetWithRRSIG const& rrset_with_rrsig) + { + for (auto& key : rrset_with_rrsig.dnskeys) { + if (key.calculated_key_tag == rrset_with_rrsig.rrsig.key_tag) + return &key; + dbgln_if(DNS_DEBUG, "DNS: DNSKEY with tag {} does not match RRSIG with tag {}", key.calculated_key_tag, rrset_with_rrsig.rrsig.key_tag); + } + return nullptr; + } + + NonnullRefPtr> validate_rrset_with_rrsig(CanonicalizedRRSetWithRRSIG rrset_with_rrsig, NonnullRefPtr result) + { + auto promise = Core::Promise::construct(); + auto& rrsig = rrset_with_rrsig.rrsig; + + Vector canon_encoded_rrs; + auto total_size = 0uz; + for (auto& rr : rrset_with_rrsig.rrset) { + rr.ttl = rrsig.original_ttl; + canon_encoded_rrs.empend(); + auto& canon_encoded_rr = canon_encoded_rrs.last(); + TRY_OR_REJECT_PROMISE(promise, rr.to_raw(canon_encoded_rr)); + total_size += canon_encoded_rr.size(); + } + quick_sort(canon_encoded_rrs, [](auto const& a, auto const& b) { + return memcmp(a.data(), b.data(), min(a.size(), b.size())) < 0; + }); + + ByteBuffer canon_encoded; + TRY_OR_REJECT_PROMISE(promise, canon_encoded.try_ensure_capacity(total_size)); + for (auto& rr : canon_encoded_rrs) + canon_encoded.append(rr); + + auto& dnskey = *find_dnskey(rrset_with_rrsig); + + if constexpr (DNS_DEBUG) { + dbgln("Validating RRSet with RRSIG for {}", result->name().to_string()); + for (auto& rr : rrset_with_rrsig.rrset) + dbgln("- RR {}", rr.to_string()); + for (auto& canon : canon_encoded_rrs) { + FixedMemoryStream stream { canon.bytes() }; + CountingStream rr_counting_stream { MaybeOwned(stream) }; + DNS::Messages::ParseContext rr_ctx { rr_counting_stream, make>() }; + auto maybe_decoded = Messages::ResourceRecord::from_raw(rr_ctx); + if (maybe_decoded.is_error()) + dbgln("-- Failed to decode RR: {}", maybe_decoded.error()); + else + dbgln("-- Canon encoded (decoded): {}", maybe_decoded.value().to_string()); + } + dbgln("- DNSKEY {}", dnskey.to_string()); + dbgln("- RRSIG {}", rrsig.to_string()); + } + + ByteBuffer to_be_signed; + { + // 2 bytes: type_covered + // 1 byte : algorithm + // 1 byte : labels + // 4 bytes: original_ttl + // 4 bytes: signature_expiration + // 4 bytes: signature_inception + // 2 bytes: key_tag + // (wire-format encoded signer name) + to_be_signed = TRY_OR_REJECT_PROMISE(promise, ByteBuffer::create_uninitialized(2 + 1 + 1 + 4 + 4 + 4 + 2)); + + auto write_u16_be = [&](size_t offset, u16 value) { + to_be_signed.bytes()[offset + 0] = (value >> 8) & 0xff; + to_be_signed.bytes()[offset + 1] = (value >> 0) & 0xff; + }; + auto write_u32_be = [&](size_t offset, u32 value) { + to_be_signed.bytes()[offset + 0] = (value >> 24) & 0xff; + to_be_signed.bytes()[offset + 1] = (value >> 16) & 0xff; + to_be_signed.bytes()[offset + 2] = (value >> 8) & 0xff; + to_be_signed.bytes()[offset + 3] = (value >> 0) & 0xff; + }; + + size_t offset = 0; + write_u16_be(offset, to_underlying(rrsig.type_covered)); + offset += 2; + to_be_signed[offset++] = static_cast(rrsig.algorithm); + to_be_signed[offset++] = rrsig.label_count; + write_u32_be(offset, rrsig.original_ttl); + offset += 4; + write_u32_be(offset, rrsig.expiration.seconds_since_epoch()); + offset += 4; + write_u32_be(offset, rrsig.inception.seconds_since_epoch()); + offset += 4; + write_u16_be(offset, rrsig.key_tag); + } + + TRY_OR_REJECT_PROMISE(promise, rrsig.signers_name.to_raw(to_be_signed)); + TRY_OR_REJECT_PROMISE(promise, to_be_signed.try_append(canon_encoded.data(), canon_encoded.size())); + + dbgln_if(DNS_DEBUG, "To be signed: {:hex-dump}", to_be_signed.bytes()); + + switch (dnskey.algorithm) { + case Messages::DNSSEC::Algorithm::RSAMD5: { + auto md5 = Crypto::Hash::MD5::create(); + md5->update(to_be_signed.data(), to_be_signed.size()); + auto digest = md5->digest(); + + auto public_key = TRY_OR_REJECT_PROMISE(promise, Crypto::PK::RSA::parse_rsa_key(dnskey.public_key, false, {})); + + auto const& signature_data = rrsig.signature; // ByteBuffer with raw RSA/MD5 signature + if (signature_data.is_empty()) { + promise->reject(Error::from_string_literal("RRSIG has an empty signature")); + return promise; + } + + Crypto::PK::RSA_PKCS1_EME rsa { public_key }; + if (auto const ok = TRY_OR_REJECT_PROMISE(promise, rsa.verify(digest.bytes(), signature_data)); !ok) { + promise->reject(Error::from_string_literal("RSA/MD5 signature validation failed")); + return promise; + } + + break; + } + case Messages::DNSSEC::Algorithm::ECDSAP256SHA256: { + auto sha256 = Crypto::Hash::SHA256::hash(to_be_signed); + auto keys = TRY_OR_REJECT_PROMISE(promise, Crypto::PK::EC::parse_ec_key(dnskey.public_key, false, {})); + auto signature = TRY_OR_REJECT_PROMISE(promise, Crypto::Curves::SECPxxxr1Signature::from_raw(Crypto::ASN1::secp256r1_oid, rrsig.signature)); + Crypto::Curves::SECP256r1 curve; + if (auto ok = TRY_OR_REJECT_PROMISE(promise, curve.verify(sha256.bytes(), keys.public_key.to_secpxxxr1_point(), signature)); !ok) { + promise->reject(Error::from_string_literal("ECDSA/SHA256 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::ECDSAP384SHA384: { + auto sha384 = Crypto::Hash::SHA384::hash(to_be_signed); + auto keys = TRY_OR_REJECT_PROMISE(promise, Crypto::PK::EC::parse_ec_key(dnskey.public_key, false, {})); + auto signature = TRY_OR_REJECT_PROMISE(promise, Crypto::Curves::SECPxxxr1Signature::from_raw(Crypto::ASN1::secp384r1_oid, rrsig.signature)); + Crypto::Curves::SECP384r1 curve; + if (auto ok = TRY_OR_REJECT_PROMISE(promise, curve.verify(sha384.bytes(), keys.public_key.to_secpxxxr1_point(), signature)); !ok) { + promise->reject(Error::from_string_literal("ECDSA/SHA384 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::RSASHA512: { + auto n = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_modulus()); + auto e = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_exponent()); + Crypto::PK::RSA_PKCS1_EMSA rsa { Crypto::Hash::HashKind::SHA512, Crypto::PK::RSAPublicKey { move(n), move(e) } }; + if (auto ok = TRY_OR_REJECT_PROMISE(promise, rsa.verify(to_be_signed, rrsig.signature)); !ok) { + promise->reject(Error::from_string_literal("RSA/SHA512 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::RSASHA1: { + auto n = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_modulus()); + auto e = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_exponent()); + Crypto::PK::RSA_PKCS1_EMSA rsa { Crypto::Hash::HashKind::SHA1, Crypto::PK::RSAPublicKey { move(n), move(e) } }; + if (auto ok = TRY_OR_REJECT_PROMISE(promise, rsa.verify(to_be_signed, rrsig.signature)); !ok) { + promise->reject(Error::from_string_literal("RSA/SHA1 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::RSASHA256: { + auto n = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_modulus()); + auto e = Crypto::UnsignedBigInteger::import_data(dnskey.public_key_rsa_exponent()); + Crypto::PK::RSA_PKCS1_EMSA rsa { Crypto::Hash::HashKind::SHA256, Crypto::PK::RSAPublicKey { move(n), move(e) } }; + if (auto ok = TRY_OR_REJECT_PROMISE(promise, rsa.verify(to_be_signed, rrsig.signature)); !ok) { + promise->reject(Error::from_string_literal("RSA/SHA256 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::ED25519: { + Crypto::Curves::Ed25519 ed25519; + if (!TRY_OR_REJECT_PROMISE(promise, ed25519.verify(dnskey.public_key.bytes(), rrsig.signature.bytes(), to_be_signed.bytes()))) { + promise->reject(Error::from_string_literal("ED25519 signature validation failed")); + return promise; + } + break; + } + case Messages::DNSSEC::Algorithm::DSA: + case Messages::DNSSEC::Algorithm::RSASHA1NSEC3SHA1: + // Not implemented yet. + case Messages::DNSSEC::Algorithm::Unknown: + dbgln("DNS: Unsupported algorithm for DNSSEC validation: {}", to_string(dnskey.algorithm)); + promise->reject(Error::from_string_literal("Unsupported algorithm for DNSSEC validation")); + break; + } + + // If we haven't rejected by now, we consider the RRSet valid. + if (!promise->is_rejected()) { + // Typically you'd store these validated RRs in the lookup result. + for (auto& record : rrset_with_rrsig.rrset) + result->add_record(move(record)); + + // Resolve with an empty success. + promise->resolve({}); + } + + return promise; } bool has_connection(bool attempt_restart = true) @@ -545,3 +1157,5 @@ private: }; } + +#undef TRY_OR_REJECT_PROMISE diff --git a/Utilities/dns.cpp b/Utilities/dns.cpp index 45a8b50f150..f3ea80bec02 100644 --- a/Utilities/dns.cpp +++ b/Utilities/dns.cpp @@ -22,11 +22,13 @@ ErrorOr serenity_main(Main::Arguments arguments) StringView server_address; StringView cert_path; bool use_tls = false; + bool dnssec = false; Core::ArgsParser args_parser; args_parser.add_option(cert_path, "Path to the CA certificate file", "ca-certs", 'C', "file"); args_parser.add_option(server_address, "The address of the DNS server to query", "server", 's', "addr"); args_parser.add_option(use_tls, "Use TLS to connect to the server", "tls", 0); + args_parser.add_option(dnssec, "Validate DNSSEC records locally", "dnssec", 0); args_parser.add_positional_argument(Core::ArgsParser::Arg { .help_string = "The resource types and name of the DNS record to query", .name = "rr,rr@name", @@ -105,7 +107,7 @@ ErrorOr serenity_main(Main::Arguments arguments) size_t pending_requests = requests.size(); for (auto& request : requests) { - resolver.lookup(request.name, DNS::Messages::Class::IN, request.types) + resolver.lookup(request.name, DNS::Messages::Class::IN, request.types, { .validate_dnssec_locally = dnssec }) ->when_resolved([&](auto& result) { outln("Resolved {}:", request.name); HashTable types;