diff --git a/api/openapi.yaml b/api/openapi.yaml index 8463007..23a189f 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -942,6 +942,10 @@ components: $ref: '#/components/schemas/DMARCRecord' bimi_record: $ref: '#/components/schemas/BIMIRecord' + dnssec_enabled: + type: boolean + description: Whether the From domain has DNSSEC enabled with valid chain of trust + example: true ptr_records: type: array items: diff --git a/go.mod b/go.mod index ebf21a7..85be917 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/miekg/dns v1.1.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -55,6 +56,7 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/peterzen/goresolver v1.0.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.56.0 // indirect github.com/redis/go-redis/v9 v9.16.0 // indirect diff --git a/go.sum b/go.sum index cd951e8..825604f 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= +github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -154,6 +156,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/peterzen/goresolver v1.0.2 h1:UxRxk835Onz7Go4oPUsOptSmBlIvN/yJ2kv3Srr3hw4= +github.com/peterzen/goresolver v1.0.2/go.mod h1:LrWRiOeCYApgvR2OhpipNOeaE1yGfI+QQjpF0riJC8M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -198,6 +202,7 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -207,6 +212,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -216,12 +222,14 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 3098934..cb1fa68 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -127,6 +127,12 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic // Check BIMI record (for From domain - branding is based on visible sender) results.BimiRecord = d.checkBIMIRecord(fromDomain, "default") + // Check DNSSEC status (for From domain) + dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, fromDomain) + if err == nil { + results.DnssecEnabled = &dnssecEnabled + } + return results } @@ -149,6 +155,12 @@ func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults { // Check BIMI record with default selector results.BimiRecord = d.checkBIMIRecord(domain, "default") + // Check DNSSEC status + dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, domain) + if err == nil { + results.DnssecEnabled = &dnssecEnabled + } + return results } @@ -204,11 +216,16 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults, senderIP string score := 0 + // DNSSEC: 10 points + if results.DnssecEnabled != nil && *results.DnssecEnabled { + score += 10 + } + // PTR and Forward DNS: 20 points score += 20 * d.calculatePTRScore(results, senderIP) / 100 - // MX Records: 20 points (10 for From domain, 10 for Return-Path domain) - score += 20 * d.calculateMXScore(results) / 100 + // MX Records: 10 points (5 for From domain, 5 for Return-Path domain) + score += 10 * d.calculateMXScore(results) / 100 // SPF Records: 20 points score += 20 * d.calculateSPFScore(results) / 100 diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go index f60484f..dcbca59 100644 --- a/pkg/analyzer/dns_resolver.go +++ b/pkg/analyzer/dns_resolver.go @@ -23,7 +23,12 @@ package analyzer import ( "context" + "fmt" "net" + "strings" + + "github.com/miekg/dns" + "github.com/peterzen/goresolver" ) // DNSResolver defines the interface for DNS resolution operations. @@ -43,38 +48,190 @@ type DNSResolver interface { // LookupHost looks up the given hostname using the local resolver. // It returns a slice of that host's addresses (IPv4 and IPv6). LookupHost(ctx context.Context, host string) ([]string, error) + + // IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records. + // Returns true if the domain has DNSSEC configured and the chain of trust is valid. + IsDNSSECEnabled(ctx context.Context, domain string) (bool, error) } -// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver. +// StandardDNSResolver is the default DNS resolver implementation that uses goresolver with DNSSEC validation. type StandardDNSResolver struct { - resolver *net.Resolver + resolver *goresolver.Resolver } -// NewStandardDNSResolver creates a new StandardDNSResolver with default settings. +// NewStandardDNSResolver creates a new StandardDNSResolver with DNSSEC validation support. func NewStandardDNSResolver() DNSResolver { + // Pass /etc/resolv.conf to load default DNS configuration + resolver, err := goresolver.NewResolver("/etc/resolv.conf") + if err != nil { + panic(fmt.Sprintf("failed to initialize goresolver: %v", err)) + } + return &StandardDNSResolver{ - resolver: &net.Resolver{ - PreferGo: true, - }, + resolver: resolver, } } -// LookupMX implements DNSResolver.LookupMX using net.Resolver. +// LookupMX implements DNSResolver.LookupMX using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { - return r.resolver.LookupMX(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeMX) + if err != nil { + return nil, err + } + + mxRecords := make([]*net.MX, 0, len(rrs)) + for _, rr := range rrs { + if mx, ok := rr.(*dns.MX); ok { + mxRecords = append(mxRecords, &net.MX{ + Host: strings.TrimSuffix(mx.Mx, "."), + Pref: mx.Preference, + }) + } + } + + if len(mxRecords) == 0 { + return nil, fmt.Errorf("no MX records found for %s", name) + } + + return mxRecords, nil } -// LookupTXT implements DNSResolver.LookupTXT using net.Resolver. +// LookupTXT implements DNSResolver.LookupTXT using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { - return r.resolver.LookupTXT(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeTXT) + if err != nil { + return nil, err + } + + txtRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if txt, ok := rr.(*dns.TXT); ok { + // Join all TXT strings (a single TXT record can have multiple strings) + txtRecords = append(txtRecords, strings.Join(txt.Txt, "")) + } + } + + if len(txtRecords) == 0 { + return nil, fmt.Errorf("no TXT records found for %s", name) + } + + return txtRecords, nil } -// LookupAddr implements DNSResolver.LookupAddr using net.Resolver. +// LookupAddr implements DNSResolver.LookupAddr using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) { - return r.resolver.LookupAddr(ctx, addr) + // Convert IP address to reverse DNS name (e.g., 1.0.0.127.in-addr.arpa.) + arpa, err := dns.ReverseAddr(addr) + if err != nil { + return nil, fmt.Errorf("invalid IP address: %w", err) + } + + rrs, err := r.resolver.StrictNSQuery(arpa, dns.TypePTR) + if err != nil { + return nil, err + } + + ptrRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if ptr, ok := rr.(*dns.PTR); ok { + ptrRecords = append(ptrRecords, strings.TrimSuffix(ptr.Ptr, ".")) + } + } + + if len(ptrRecords) == 0 { + return nil, fmt.Errorf("no PTR records found for %s", addr) + } + + return ptrRecords, nil } -// LookupHost implements DNSResolver.LookupHost using net.Resolver. +// LookupHost implements DNSResolver.LookupHost using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) { - return r.resolver.LookupHost(ctx, host) + // Ensure the host ends with a dot for DNS queries + queryName := host + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + var allAddrs []string + + // Query A records (IPv4) + rrsA, errA := r.resolver.StrictNSQuery(queryName, dns.TypeA) + if errA == nil { + for _, rr := range rrsA { + if a, ok := rr.(*dns.A); ok { + allAddrs = append(allAddrs, a.A.String()) + } + } + } + + // Query AAAA records (IPv6) + rrsAAAA, errAAAA := r.resolver.StrictNSQuery(queryName, dns.TypeAAAA) + if errAAAA == nil { + for _, rr := range rrsAAAA { + if aaaa, ok := rr.(*dns.AAAA); ok { + allAddrs = append(allAddrs, aaaa.AAAA.String()) + } + } + } + + // Return error only if both queries failed + if errA != nil && errAAAA != nil { + return nil, fmt.Errorf("failed to resolve host: IPv4 error: %v, IPv6 error: %v", errA, errAAAA) + } + + if len(allAddrs) == 0 { + return nil, fmt.Errorf("no A or AAAA records found for %s", host) + } + + return allAddrs, nil +} + +// IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records. +// It uses DNSSEC validation to ensure the chain of trust is valid. +// Returns true if DNSSEC is properly configured and validated, false otherwise. +func (r *StandardDNSResolver) IsDNSSECEnabled(ctx context.Context, domain string) (bool, error) { + // Ensure the domain ends with a dot for DNS queries + queryName := domain + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + // Query for DNSKEY records with DNSSEC validation + // If this succeeds, it means: + // 1. The domain has DNSKEY records (DNSSEC is configured) + // 2. The DNSSEC chain of trust is valid (validated by StrictNSQuery) + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeDNSKEY) + if err != nil { + // DNSSEC is not enabled or validation failed + return false, nil + } + + // Check if we got any DNSKEY records + if len(rrs) == 0 { + return false, nil + } + + // Verify we actually have DNSKEY records (not just any RR type) + hasDNSKEY := false + for _, rr := range rrs { + if _, ok := rr.(*dns.DNSKEY); ok { + hasDNSKEY = true + break + } + } + + return hasDNSKEY, nil } diff --git a/pkg/analyzer/dns_resolver_test.go b/pkg/analyzer/dns_resolver_test.go new file mode 100644 index 0000000..7c9091b --- /dev/null +++ b/pkg/analyzer/dns_resolver_test.go @@ -0,0 +1,111 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package analyzer + +import ( + "context" + "testing" +) + +func TestIsDNSSECEnabled(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + tests := []struct { + name string + domain string + expectDNSSEC bool + }{ + { + name: "ietf.org has DNSSEC", + domain: "ietf.org", + expectDNSSEC: true, + }, + { + name: "google.com doesn't have DNSSEC", + domain: "google.com", + expectDNSSEC: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enabled, err := resolver.IsDNSSECEnabled(ctx, tt.domain) + if err != nil { + t.Errorf("IsDNSSECEnabled() error = %v", err) + return + } + + if enabled != tt.expectDNSSEC { + t.Errorf("IsDNSSECEnabled() for %s = %v, want %v", tt.domain, enabled, tt.expectDNSSEC) + } else { + // Log the result even if we're not validating + if enabled { + t.Logf("%s: DNSSEC is enabled ✅", tt.domain) + } else { + t.Logf("%s: DNSSEC is NOT enabled ⚠️", tt.domain) + } + } + }) + } +} + +func TestIsDNSSECEnabled_NonExistentDomain(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + // Test with a domain that doesn't exist + enabled, err := resolver.IsDNSSECEnabled(ctx, "this-domain-definitely-does-not-exist-12345.com") + if err != nil { + // Error is acceptable for non-existent domains + t.Logf("Non-existent domain returned error (expected): %v", err) + return + } + + // If no error, DNSSEC should be disabled + if enabled { + t.Error("IsDNSSECEnabled() for non-existent domain should return false") + } +} + +func TestIsDNSSECEnabled_WithTrailingDot(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + // Test that both formats work + domain1 := "cloudflare.com" + domain2 := "cloudflare.com." + + enabled1, err1 := resolver.IsDNSSECEnabled(ctx, domain1) + if err1 != nil { + t.Errorf("IsDNSSECEnabled() without trailing dot error = %v", err1) + } + + enabled2, err2 := resolver.IsDNSSECEnabled(ctx, domain2) + if err2 != nil { + t.Errorf("IsDNSSECEnabled() with trailing dot error = %v", err2) + } + + if enabled1 != enabled2 { + t.Errorf("IsDNSSECEnabled() results differ: without dot = %v, with dot = %v", enabled1, enabled2) + } +} diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index 337f7c1..1bf02f7 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -10,6 +10,7 @@ import BimiRecordDisplay from "./BimiRecordDisplay.svelte"; import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte"; import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte"; + import DnssecDisplay from "./DnssecDisplay.svelte"; interface Props { domainAlignment?: DomainAlignment; @@ -150,6 +151,9 @@ + + + {/if} diff --git a/web/src/lib/components/DnssecDisplay.svelte b/web/src/lib/components/DnssecDisplay.svelte new file mode 100644 index 0000000..a795ebd --- /dev/null +++ b/web/src/lib/components/DnssecDisplay.svelte @@ -0,0 +1,56 @@ + + +{#if dnssecEnabled !== undefined} +
+
+
+ + DNSSEC +
+ Security +
+
+

+ DNSSEC (DNS Security Extensions) adds cryptographic signatures to DNS records to verify + their authenticity and integrity. It protects against DNS spoofing and cache poisoning + attacks, ensuring that DNS responses haven't been tampered with. +

+ {#if domain} +
+ Domain: {domain} +
+ {/if} + {#if dnssecIsValid} +
+ + Enabled: DNSSEC is properly configured with a valid chain of trust. + This provides additional security and authenticity for your domain's DNS records. +
+ {:else} +
+ + Not Enabled: DNSSEC is not configured for this domain. While not + required for email delivery, enabling DNSSEC provides additional security by protecting + against DNS-based attacks. Consider enabling DNSSEC through your domain registrar or + DNS provider. +
+ {/if} +
+
+{/if}