From 5bb6d81dd8ab704e245ca830a2ae8787bb51e0c7 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:20:39 -0700 Subject: [PATCH 01/12] refactor: return raw dns response from auxiliary functions --- src/zdns/lookup.go | 57 +++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index c61b782f..450bcd02 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -540,24 +540,25 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam return &SingleQueryResult{}, false, StatusError, fmt.Errorf("no connection info for nameserver: %s", nameServer) } var result *SingleQueryResult + var rawResp *dns.Msg var status Status if r.dnsOverHTTPSEnabled { r.verboseLog(1, "****WIRE LOOKUP*** ", DoHProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) - result, status, err = doDoHLookup(lookupCtx, connInfo.httpsClient, q, nameServer, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) + result, rawResp, status, err = doDoHLookup(lookupCtx, connInfo.httpsClient, q, nameServer, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) } else if r.dnsOverTLSEnabled { r.verboseLog(1, "****WIRE LOOKUP*** ", DoTProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) - result, status, err = doDoTLookup(lookupCtx, connInfo, q, nameServer, r.rootCAs, r.verifyServerCert, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) + result, rawResp, status, err = doDoTLookup(lookupCtx, connInfo, q, nameServer, r.rootCAs, r.verifyServerCert, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) } else if connInfo.udpClient != nil { r.verboseLog(1, "****WIRE LOOKUP*** ", UDPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) - result, status, err = wireLookupUDP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) + result, rawResp, status, err = wireLookupUDP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) if status == StatusTruncated && connInfo.tcpClient != nil { // result truncated, try again with TCP r.verboseLog(1, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) - result, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) + result, rawResp, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) } } else if connInfo.tcpClient != nil { r.verboseLog(1, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) - result, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) + result, rawResp, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) } else { return &SingleQueryResult{}, false, StatusError, errors.New("no connection info for nameserver") } @@ -580,7 +581,7 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam return result, isCached, status, err } -func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, rootCAs *x509.CertPool, shouldVerifyServerCert, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (*SingleQueryResult, Status, error) { +func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, rootCAs *x509.CertPool, shouldVerifyServerCert, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (*SingleQueryResult, *dns.Msg, Status, error) { m := new(dns.Msg) m.SetQuestion(dotName(q.Name), q.Type) m.Question[0].Qclass = q.Class @@ -613,7 +614,7 @@ func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, name } tcpConn, err := dialer.DialContext(ctx, "tcp", nameServer.String()) if err != nil { - return nil, StatusError, errors.Wrap(err, "could not connect to server") + return nil, nil, StatusError, errors.Wrap(err, "could not connect to server") } // Now wrap the connection with TLS tlsConn := tls.Client(tcpConn, &tls.Config{ @@ -633,18 +634,18 @@ func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, name if closeErr != nil { log.Errorf("error closing TLS connection: %v", err) } - return nil, StatusError, errors.Wrap(err, "could not perform TLS handshake") + return nil, nil, StatusError, errors.Wrap(err, "could not perform TLS handshake") } connInfo.tlsHandshake = tlsConn.GetHandshakeLog() connInfo.tlsConn = &dns.Conn{Conn: tlsConn} } err := connInfo.tlsConn.WriteMsg(m) if err != nil { - return nil, "", errors.Wrap(err, "could not write query over DoT to server") + return nil, nil, "", errors.Wrap(err, "could not write query over DoT to server") } responseMsg, err := connInfo.tlsConn.ReadMsg() if err != nil { - return nil, StatusError, errors.Wrap(err, "could not unpack DNS message from DoT server") + return nil, nil, StatusError, errors.Wrap(err, "could not unpack DNS message from DoT server") } res := SingleQueryResult{ Resolver: connInfo.tlsConn.Conn.RemoteAddr().String(), @@ -666,7 +667,7 @@ func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, name return constructSingleQueryResultFromDNSMsg(&res, responseMsg) } -func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameServer *NameServer, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (*SingleQueryResult, Status, error) { +func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameServer *NameServer, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (*SingleQueryResult, *dns.Msg, Status, error) { m := new(dns.Msg) m.SetQuestion(dotName(q.Name), q.Type) m.Question[0].Qclass = q.Class @@ -679,10 +680,10 @@ func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameS } bytes, err := m.Pack() if err != nil { - return nil, StatusError, errors.Wrap(err, "could not pack DNS message") + return nil, nil, StatusError, errors.Wrap(err, "could not pack DNS message") } if strings.Contains(nameServer.DomainName, "http://") { - return nil, StatusError, errors.New("DoH name server must use HTTPS") + return nil, nil, StatusError, errors.New("DoH name server must use HTTPS") } httpsDomain := nameServer.DomainName if !strings.HasPrefix(httpsDomain, "https://") { @@ -693,14 +694,14 @@ func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameS } req, err := http.NewRequest("POST", httpsDomain, strings.NewReader(string(bytes))) if err != nil { - return nil, StatusError, errors.Wrap(err, "could not create HTTP request") + return nil, nil, StatusError, errors.Wrap(err, "could not create HTTP request") } req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") req = req.WithContext(ctx) resp, err := httpClient.Do(req) if err != nil { - return nil, StatusError, errors.Wrap(err, "could not perform HTTP request") + return nil, nil, StatusError, errors.Wrap(err, "could not perform HTTP request") } defer func(Body io.ReadCloser) { err = Body.Close() @@ -710,13 +711,13 @@ func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameS }(resp.Body) bytes, err = io.ReadAll(resp.Body) if err != nil { - return nil, StatusError, errors.Wrap(err, "could not read HTTP response") + return nil, nil, StatusError, errors.Wrap(err, "could not read HTTP response") } r := new(dns.Msg) err = r.Unpack(bytes) if err != nil { - return nil, StatusError, errors.Wrap(err, "could not unpack DNS message") + return nil, nil, StatusError, errors.Wrap(err, "could not unpack DNS message") } res := SingleQueryResult{ Resolver: nameServer.DomainName, @@ -738,7 +739,7 @@ func doDoHLookup(ctx context.Context, httpClient *http.Client, q Question, nameS } // wireLookupTCP performs a DNS lookup on-the-wire over TCP with the given parameters -func wireLookupTCP(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, ednsOptions []dns.EDNS0, recursive, dnssec, checkingDisabled bool) (*SingleQueryResult, Status, error) { +func wireLookupTCP(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, ednsOptions []dns.EDNS0, recursive, dnssec, checkingDisabled bool) (*SingleQueryResult, *dns.Msg, Status, error) { res := SingleQueryResult{Answers: []interface{}{}, Authorities: []interface{}{}, Additional: []interface{}{}} res.Resolver = nameServer.String() @@ -761,7 +762,7 @@ func wireLookupTCP(ctx context.Context, connInfo *ConnectionInfo, q Question, na var addr *net.TCPAddr addr, err = net.ResolveTCPAddr("tcp", nameServer.String()) if err != nil { - return nil, StatusError, fmt.Errorf("could not resolve TCP address %s: %v", nameServer.String(), err) + return nil, nil, StatusError, fmt.Errorf("could not resolve TCP address %s: %v", nameServer.String(), err) } r, _, err = connInfo.tcpClient.ExchangeWithConnToContext(ctx, m, connInfo.tcpConn, addr) if err != nil && err.Error() == "EOF" { @@ -782,17 +783,17 @@ func wireLookupTCP(ctx context.Context, connInfo *ConnectionInfo, q Question, na if err != nil || r == nil { if nerr, ok := err.(net.Error); ok { if nerr.Timeout() { - return &res, StatusTimeout, nil + return &res, r, StatusTimeout, nil } } - return &res, StatusError, err + return &res, r, StatusError, err } return constructSingleQueryResultFromDNSMsg(&res, r) } // wireLookupUDP performs a DNS lookup on-the-wire over UDP with the given parameters -func wireLookupUDP(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, ednsOptions []dns.EDNS0, recursive, dnssec, checkingDisabled bool) (*SingleQueryResult, Status, error) { +func wireLookupUDP(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, ednsOptions []dns.EDNS0, recursive, dnssec, checkingDisabled bool) (*SingleQueryResult, *dns.Msg, Status, error) { res := SingleQueryResult{Answers: []interface{}{}, Authorities: []interface{}{}, Additional: []interface{}{}} res.Resolver = nameServer.String() res.Protocol = "udp" @@ -817,22 +818,22 @@ func wireLookupUDP(ctx context.Context, connInfo *ConnectionInfo, q Question, na r, _, err = connInfo.udpClient.ExchangeContext(ctx, m, nameServer.String()) } if r != nil && (r.Truncated || r.Rcode == dns.RcodeBadTrunc) { - return &res, StatusTruncated, err + return &res, r, StatusTruncated, err } if err != nil || r == nil { if nerr, ok := err.(net.Error); ok { if nerr.Timeout() { - return &res, StatusTimeout, nil + return &res, r, StatusTimeout, nil } } - return &res, StatusError, err + return &res, r, StatusError, err } return constructSingleQueryResultFromDNSMsg(&res, r) } // fills out all the fields in a SingleQueryResult from a dns.Msg directly. -func constructSingleQueryResultFromDNSMsg(res *SingleQueryResult, r *dns.Msg) (*SingleQueryResult, Status, error) { +func constructSingleQueryResultFromDNSMsg(res *SingleQueryResult, r *dns.Msg) (*SingleQueryResult, *dns.Msg, Status, error) { if r.Rcode != dns.RcodeSuccess { for _, ans := range r.Extra { inner := ParseAnswer(ans) @@ -840,7 +841,7 @@ func constructSingleQueryResultFromDNSMsg(res *SingleQueryResult, r *dns.Msg) (* res.Additional = append(res.Additional, inner) } } - return res, TranslateDNSErrorCode(r.Rcode), nil + return res, r, TranslateDNSErrorCode(r.Rcode), nil } res.Flags.Response = r.Response @@ -871,7 +872,7 @@ func constructSingleQueryResultFromDNSMsg(res *SingleQueryResult, r *dns.Msg) (* res.Authorities = append(res.Authorities, inner) } } - return res, StatusNoError, nil + return res, r, StatusNoError, nil } func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *QuestionWithMetadata, depth int, result *SingleQueryResult, layer string, trace Trace) (*SingleQueryResult, Trace, Status, error) { From 50707609b6fc775d17a665fd5011da5849526e09 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:03:05 -0700 Subject: [PATCH 02/12] feat: basic, same-level DNSSEC validation --- src/zdns/lookup.go | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index 450bcd02..805fbd23 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -33,6 +33,11 @@ import ( "github.com/zmap/zdns/src/internal/util" ) +const ( + zoneSigningKeyFlag = 256 + keySigningKeyFlag = 257 +) + // GetDNSServers returns a list of IPv4, IPv6 DNS servers from a file, or an error if one occurs func GetDNSServers(path string) (ipv4, ipv6 []string, err error) { c, err := dns.ClientConfigFromFile(path) @@ -569,6 +574,14 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam r.verboseLog(depth+2, "Results from wire for name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer, " status: ", status, " , err: ", err, " result: ", result) if status == StatusNoError && result != nil { + if r.dnsSecEnabled { + validated, err := r.validateChainOfDNSSECTrust(ctx, rawResp, q, nameServer, !requestIteration, depth+2) + r.verboseLog(depth+2, "DNSSEC validation result: ", validated, " err: ", err) + if err != nil { + return result, isCached, StatusError, errors.Wrap(err, "could not validate chain of DNSSEC trust") + } + } + // only cache answers that don't have errors if !requestIteration && strings.ToLower(q.Name) != layer && authName != layer && !result.Flags.Authoritative { // TODO - how to detect if we've retrieved an authority record or a answer record? maybe add q.Name != authName r.verboseLog(depth+2, "Cache auth upsert for ", authName) @@ -992,6 +1005,122 @@ func (r *Resolver) extractAuthority(ctx context.Context, authority interface{}, return nil, StatusServFail, layer, trace } +func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, q Question, nameServer *NameServer, isIterative bool, depth int) (bool, error) { + typeToRRSets := make(map[uint16][]dns.RR) + typeToRRSigs := make(map[uint16][]*dns.RRSIG) + + // Extract all the RRSets from the message + for _, rr := range msg.Answer { + rrType := rr.Header().Rrtype + if rrType == dns.TypeRRSIG { + rrSig := rr.(*dns.RRSIG) + typeToRRSigs[rrSig.TypeCovered] = append(typeToRRSigs[rrSig.TypeCovered], rrSig) + } else { + typeToRRSets[rrType] = append(typeToRRSets[rrType], rr) + } + } + + // Shortcut checks on RRSIG cardinality + if len(typeToRRSigs) == 0 { + // No RRSIGs, possibly because DNSSEC is not enabled... or we are hijacked + r.verboseLog(depth+1, "DNSSEC: No RRSIGs found") + return false, nil + } + + if len(typeToRRSets) != len(typeToRRSigs) { + return false, errors.New("mismatched number of RRsets and RRSIGs") + } + + // Verify if for each RRset there is a corresponding RRSIG + for rrType := range typeToRRSets { + if _, ok := typeToRRSigs[rrType]; !ok { + return false, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) + } + } + + r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found %d RRsets and %d RRSIGs", len(typeToRRSets), len(typeToRRSigs))) + + if q.Type == dns.TypeDNSKEY { + // Find Key Signing Key (KSK) and Zone Signing Key (ZSK) + var ksk *dns.DNSKEY + for _, rr := range typeToRRSets[dns.TypeDNSKEY] { + dnskey := rr.(*dns.DNSKEY) + if dnskey.Flags == keySigningKeyFlag { + ksk = dnskey + } else if dnskey.Flags == zoneSigningKeyFlag { + continue + } else { + return false, fmt.Errorf("unknown DNSKEY flag: %d", dnskey.Flags) + } + } + + if ksk == nil { + return false, errors.New("could not find KSK") + } + + // Verify the RRSIGs for the DNSKEY RRset + // TODO: key tag? + if err := typeToRRSigs[dns.TypeDNSKEY][0].Verify(ksk, typeToRRSets[dns.TypeDNSKEY]); err != nil { + return false, errors.Wrap(err, "could not verify DNSKEY RRSIG") + } + + // TODO: we'll just trust KSK for now (but we should find DS record and verify it) + + return true, nil + } + + // Gather DNSKEYs + // TODO: > 1 ZSK? + var zsk *dns.DNSKEY + retries := r.retries + dnskeyQuestion := QuestionWithMetadata{ + Q: Question{ + Name: q.Name, + Type: dns.TypeDNSKEY, + Class: q.Class, + }, + RetriesRemaining: &retries, + } + res, _, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative) + if status != StatusNoError || err != nil { + return false, fmt.Errorf("cannot get DNSKEYs, status: %s, err: %v", status, err) + } + + // Type converting ZSK + for _, rr := range res.Answers { + switch rr := rr.(type) { + case DNSKEYAnswer: + // We don't care about KSK here. It must have been validated during DNSKEY lookup. + if rr.Flags == 256 { // TODO: magic number + zsk = &dns.DNSKEY{ + Hdr: dns.RR_Header{Name: dns.CanonicalName(rr.Name), Rrtype: rr.RrType, Class: dns.StringToClass[rr.Class], Ttl: rr.TTL}, + Flags: rr.Flags, + Protocol: rr.Protocol, + Algorithm: rr.Algorithm, + PublicKey: rr.PublicKey, + } + } + case RRSIGAnswer: + continue // It's normal to have RRSIGs in any DNSSEC-enabled response. + default: + r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found unexpected RRset in DNSKEY answer: %v", rr)) + continue + } + } + + // Verify the RRSIGs for each RRset + for rrType, rrSet := range typeToRRSets { + sig := typeToRRSigs[rrType][0] + // TODO should make use of sig.KeyTag. + r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Verifying RRSIG for type %s", dns.TypeToString[rrType])) + if err := sig.Verify(zsk, rrSet); err != nil { + return false, errors.Wrapf(err, "could not verify RRSIG for type %s", dns.TypeToString[rrType]) + } + } + + return true, nil +} + // CheckTxtRecords common function for all modules based on search in TXT record func CheckTxtRecords(res *SingleQueryResult, status Status, regex *regexp.Regexp, err error) (string, Status, error) { if status != StatusNoError { From 2e432e7e78a0e0a3f76fdc5c45f37e0042b3426d Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:24:34 -0700 Subject: [PATCH 03/12] fix: newLayer should not be updated if extractAuthority fails --- src/zdns/lookup.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index 805fbd23..b6518cb2 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -896,13 +896,18 @@ func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *Question newTrace := trace nameServers := make([]NameServer, 0, len(result.Authorities)) for i, elem := range result.Authorities { + if _, ok := elem.(DSAnswer); ok { + // DS records are not nameservers, skip them + continue + } + if util.HasCtxExpired(&ctx) { return &SingleQueryResult{}, newTrace, StatusTimeout, nil } var ns *NameServer var nsStatus Status r.verboseLog(depth+1, "Trying Authority: ", elem) - ns, nsStatus, newLayer, newTrace = r.extractAuthority(ctx, elem, layer, qWithMeta.RetriesRemaining, depth, result, newTrace) + ns, nsStatus, nextLayer, newTrace := r.extractAuthority(ctx, elem, layer, qWithMeta.RetriesRemaining, depth, result, newTrace) r.verboseLog(depth+1, "Output from extract authorities: ", ns.String()) if nsStatus == StatusIterTimeout { r.verboseLog(depth+2, "--> Hit iterative timeout: ") @@ -921,6 +926,7 @@ func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *Question } } else { // We have a valid nameserver + newLayer = nextLayer nameServers = append(nameServers, *ns) } } From b1f0b6b7fb8c2b211ccbd428df22d74f963c30bd Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:06:23 -0700 Subject: [PATCH 04/12] refactor: extend trace, fix depth for couple places --- src/zdns/lookup.go | 109 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index b6518cb2..f1c53471 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -112,7 +112,8 @@ func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIt if r.followCNAMEs { return r.followingLookup(ctx, &questionWithMeta, nameServers, isIterative) } - res, trace, status, err := r.lookup(ctx, &questionWithMeta, nameServers, isIterative) + var trace Trace + res, trace, status, err := r.lookup(ctx, &questionWithMeta, nameServers, isIterative, trace, 1) if err != nil { return res, nil, status, fmt.Errorf("could not perform retrying lookup for name %v: %w", q.Name, err) } @@ -120,25 +121,24 @@ func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIt } // lookup performs a DNS lookup for a given question against a slice of interchangeable nameservers, taking care of iterative and external lookups -func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { +func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, isIterative bool, trace Trace, depth int) (*SingleQueryResult, Trace, Status, error) { var res *SingleQueryResult var isCached IsCached - var trace Trace var status Status var err error if util.HasCtxExpired(&ctx) { return res, trace, StatusTimeout, nil } if isIterative { - r.verboseLog(1, "MIEKG-IN: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") - res, trace, status, err = r.iterativeLookup(ctx, qWithMeta, nameServers, 1, ".", trace) - r.verboseLog(1, "MIEKG-OUT: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, "): status: ", status, " , err: ", err) + r.verboseLog(depth, "MIEKG-IN: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") + res, trace, status, err = r.iterativeLookup(ctx, qWithMeta, nameServers, depth, ".", trace) + r.verboseLog(depth, "MIEKG-OUT: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, "): status: ", status, " , err: ", err) } else { tries := 0 // external lookup - r.verboseLog(1, "MIEKG-IN: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") - res, isCached, status, err = r.cyclingLookup(ctx, qWithMeta, nameServers, qWithMeta.Q.Name, 1, true) - r.verboseLog(1, "MIEKG-OUT: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ") with ", tries, " attempts: status: ", status, " , err: ", err) + r.verboseLog(depth, "MIEKG-IN: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") + res, isCached, status, trace, err = r.cyclingLookup(ctx, qWithMeta, nameServers, qWithMeta.Q.Name, depth, true, trace) + r.verboseLog(depth, "MIEKG-OUT: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ") with ", tries, " attempts: status: ", status, " , err: ", err) var t TraceStep // TODO check for null res if res != nil { @@ -151,10 +151,10 @@ func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, t.DNSClass = qWithMeta.Q.Class t.Name = qWithMeta.Q.Name t.Layer = qWithMeta.Q.Name - t.Depth = 1 + t.Depth = depth t.Cached = isCached t.Try = tries - trace = Trace{t} + trace = append(trace, t) } return res, trace, status, err } @@ -180,11 +180,7 @@ func (r *Resolver) followingLookup(ctx context.Context, qWithMeta *QuestionWithM r.verboseLog(0, "MIEKG-IN: starting a C/DNAME following lookup for ", originalName, " (", qWithMeta.Q.Type, ")") for i := 0; i < r.maxDepth; i++ { qWithMeta.Q.Name = currName // update the question with the current name, this allows following CNAMEs - iterRes, iterTrace, iterStatus, lookupErr := r.lookup(ctx, qWithMeta, nameServers, isIterative) - // append iterTrace to the global trace so we can return full trace - if iterTrace != nil { - trace = append(trace, iterTrace...) - } + iterRes, trace, iterStatus, lookupErr := r.lookup(ctx, qWithMeta, nameServers, isIterative, trace, 1) if iterStatus != StatusNoError || lookupErr != nil { if i == 0 { // only have 1 result to return @@ -343,7 +339,7 @@ func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithM // create iteration context for this iteration step iterationStepCtx, cancel := context.WithTimeout(ctx, r.iterativeTimeout) defer cancel() - result, isCached, status, err := r.cyclingLookup(iterationStepCtx, qWithMeta, nameServers, layer, depth, false) + result, isCached, status, trace, err := r.cyclingLookup(iterationStepCtx, qWithMeta, nameServers, layer, depth, false, trace) if status == StatusNoError && result != nil { var t TraceStep t.Result = *result @@ -393,7 +389,7 @@ func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithM // cyclingLookup performs a DNS lookup against a slice of nameservers, cycling through them until a valid response is received. // If the number of retries in QuestionWithMetadata is 0, the function will return an error. -func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, layer string, depth int, recursionDesired bool) (*SingleQueryResult, IsCached, Status, error) { +func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, layer string, depth int, recursionDesired bool, trace Trace) (*SingleQueryResult, IsCached, Status, Trace, error) { var cacheBasedOnNameServer bool var cacheNonAuthoritative bool if recursionDesired { @@ -415,23 +411,23 @@ func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMet for *qWithMeta.RetriesRemaining >= 0 { if util.HasCtxExpired(&ctx) { - return &SingleQueryResult{}, false, StatusTimeout, nil + return &SingleQueryResult{}, false, StatusTimeout, trace, nil } // get random unqueried nameserver nameServer, queriedNameServers = getRandomNonQueriedNameServer(nameServers, queriedNameServers) // perform the lookup - result, isCached, status, err = r.cachedLookup(ctx, qWithMeta.Q, nameServer, layer, depth, recursionDesired, cacheBasedOnNameServer, cacheNonAuthoritative) + result, isCached, status, trace, err = r.cachedLookup(ctx, qWithMeta.Q, nameServer, layer, depth, recursionDesired, cacheBasedOnNameServer, cacheNonAuthoritative, trace) if status == StatusNoError { r.verboseLog(depth+1, "Cycling lookup successful. Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) - return result, isCached, status, err + return result, isCached, status, trace, err } else if *qWithMeta.RetriesRemaining == 0 { r.verboseLog(depth+1, "Cycling lookup failed - out of retries. Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) - return result, isCached, status, errors.New("cycling lookup failed - out of retries") + return result, isCached, status, trace, errors.New("cycling lookup failed - out of retries") } r.verboseLog(depth+1, "Cycling lookup failed, using a retry. Retries remaining: ", qWithMeta.RetriesRemaining, " , Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) *qWithMeta.RetriesRemaining-- } - return &SingleQueryResult{}, false, StatusError, errors.New("cycling lookup function did not exit properly") + return &SingleQueryResult{}, false, StatusError, trace, errors.New("cycling lookup function did not exit properly") } // getRandomNonQueriedNameServer returns a random name server from the list of name servers that has not been queried yet @@ -457,12 +453,12 @@ func getRandomNonQueriedNameServer(nameServers []NameServer, queriedNameServers // requestIteration is whether to set the "recursion desired" bit in the DNS query // cacheBasedOnNameServer is whether to consider a cache hit based on DNS question and nameserver, or just question // cacheNonAuthoritative is whether to cache non-authoritative answers, usually used for lookups using an external resolver -func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *NameServer, layer string, depth int, requestIteration, cacheBasedOnNameServer, cacheNonAuthoritative bool) (*SingleQueryResult, IsCached, Status, error) { +func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *NameServer, layer string, depth int, requestIteration, cacheBasedOnNameServer, cacheNonAuthoritative bool, trace Trace) (*SingleQueryResult, IsCached, Status, Trace, error) { var isCached IsCached isCached = false r.verboseLog(depth+1, "Cached retrying lookup. Name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer) if isValid, reason := nameServer.IsValid(); !isValid { - return &SingleQueryResult{}, false, StatusIllegalInput, fmt.Errorf("invalid nameserver (%s): %s", nameServer.String(), reason) + return &SingleQueryResult{}, false, StatusIllegalInput, trace, fmt.Errorf("invalid nameserver (%s): %s", nameServer.String(), reason) } // create a context for this network lookup lookupCtx, cancel := context.WithTimeout(ctx, r.networkTimeout) @@ -489,15 +485,15 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam // default to UDP cachedResult.Protocol = UDPProtocol } - return cachedResult, isCached, StatusNoError, nil + return cachedResult, isCached, StatusNoError, trace, nil } // Stop if we hit a nameserver we don't want to hit if r.blacklist != nil { if blacklisted, isBlacklistedErr := r.blacklist.IsBlacklisted(nameServer.IP.String()); isBlacklistedErr != nil { - return nil, isCached, StatusError, errors.Wrapf(isBlacklistedErr, "could not check blacklist for nameserver IP: %s", nameServer.IP.String()) + return nil, isCached, StatusError, trace, errors.Wrapf(isBlacklistedErr, "could not check blacklist for nameserver IP: %s", nameServer.IP.String()) } else if blacklisted { - return &SingleQueryResult{}, isCached, StatusBlacklist, nil + return &SingleQueryResult{}, isCached, StatusBlacklist, trace, nil } } var authName string @@ -512,13 +508,13 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam authName, err = nextAuthority(name, layer) if err != nil { r.verboseLog(depth+2, err) - return &SingleQueryResult{}, isCached, StatusAuthFail, errors.Wrap(err, "could not get next authority with name: "+name+" and layer: "+layer) + return &SingleQueryResult{}, isCached, StatusAuthFail, trace, errors.Wrap(err, "could not get next authority with name: "+name+" and layer: "+layer) } if name != layer && authName != layer { // we have a valid authority to check the cache for if authName == "" { r.verboseLog(depth+2, "Can't parse name to authority properly. name: ", name, ", layer: ", layer) - return &SingleQueryResult{}, isCached, StatusAuthFail, nil + return &SingleQueryResult{}, isCached, StatusAuthFail, trace, nil } r.verboseLog(depth+2, "Cache auth check for ", authName) // TODO - this will need to be changed for AllNameServers @@ -527,7 +523,7 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam r.verboseLog(depth+2, "Cache auth hit for ", authName) // only want to return if we actually have additionals and authorities from the cache for the caller if len(cachedResult.Additional) > 0 && len(cachedResult.Authorities) > 0 { - return cachedResult, true, StatusNoError, nil + return cachedResult, true, StatusNoError, trace, nil } // unsuccessful in retrieving from the cache, we'll continue to the wire } @@ -538,47 +534,47 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam r.verboseLog(depth+2, "Cache miss for ", q, ", Layer: ", layer, ", Nameserver: ", nameServer, " going to the wire in retryingLookup") connInfo, err := r.getConnectionInfo(nameServer) if err != nil { - return &SingleQueryResult{}, false, StatusError, fmt.Errorf("could not get a connection info to query nameserver %s: %v", nameServer, err) + return &SingleQueryResult{}, false, StatusError, trace, fmt.Errorf("could not get a connection info to query nameserver %s: %v", nameServer, err) } // check that our connection info is valid if connInfo == nil { - return &SingleQueryResult{}, false, StatusError, fmt.Errorf("no connection info for nameserver: %s", nameServer) + return &SingleQueryResult{}, false, StatusError, trace, fmt.Errorf("no connection info for nameserver: %s", nameServer) } var result *SingleQueryResult var rawResp *dns.Msg var status Status if r.dnsOverHTTPSEnabled { - r.verboseLog(1, "****WIRE LOOKUP*** ", DoHProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + r.verboseLog(depth, "****WIRE LOOKUP*** ", DoHProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) result, rawResp, status, err = doDoHLookup(lookupCtx, connInfo.httpsClient, q, nameServer, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) } else if r.dnsOverTLSEnabled { - r.verboseLog(1, "****WIRE LOOKUP*** ", DoTProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + r.verboseLog(depth, "****WIRE LOOKUP*** ", DoTProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) result, rawResp, status, err = doDoTLookup(lookupCtx, connInfo, q, nameServer, r.rootCAs, r.verifyServerCert, requestIteration, r.ednsOptions, r.dnsSecEnabled, r.checkingDisabledBit) } else if connInfo.udpClient != nil { - r.verboseLog(1, "****WIRE LOOKUP*** ", UDPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + r.verboseLog(depth, "****WIRE LOOKUP*** ", UDPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) result, rawResp, status, err = wireLookupUDP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) if status == StatusTruncated && connInfo.tcpClient != nil { // result truncated, try again with TCP - r.verboseLog(1, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + r.verboseLog(depth, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) result, rawResp, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) } } else if connInfo.tcpClient != nil { - r.verboseLog(1, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) + r.verboseLog(depth, "****WIRE LOOKUP*** ", TCPProtocol, " ", dns.TypeToString[q.Type], " ", q.Name, " ", nameServer) result, rawResp, status, err = wireLookupTCP(lookupCtx, connInfo, q, nameServer, r.ednsOptions, requestIteration, r.dnsSecEnabled, r.checkingDisabledBit) } else { - return &SingleQueryResult{}, false, StatusError, errors.New("no connection info for nameserver") + return &SingleQueryResult{}, false, StatusError, trace, errors.New("no connection info for nameserver") } if err != nil { - return &SingleQueryResult{}, isCached, status, errors.Wrap(err, "could not perform lookup") + return &SingleQueryResult{}, isCached, status, trace, errors.Wrap(err, "could not perform lookup") } r.verboseLog(depth+2, "Results from wire for name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer, " status: ", status, " , err: ", err, " result: ", result) if status == StatusNoError && result != nil { if r.dnsSecEnabled { - validated, err := r.validateChainOfDNSSECTrust(ctx, rawResp, q, nameServer, !requestIteration, depth+2) + validated, trace, err := r.validateChainOfDNSSECTrust(ctx, rawResp, q, nameServer, !requestIteration, depth+2, trace) r.verboseLog(depth+2, "DNSSEC validation result: ", validated, " err: ", err) if err != nil { - return result, isCached, StatusError, errors.Wrap(err, "could not validate chain of DNSSEC trust") + return result, isCached, StatusError, trace, errors.Wrap(err, "could not validate chain of DNSSEC trust") } } @@ -591,7 +587,7 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam r.cache.SafeAddCachedAnswer(q, result, cacheNameServer, layer, depth+2, cacheNonAuthoritative) } } - return result, isCached, status, err + return result, isCached, status, trace, err } func doDoTLookup(ctx context.Context, connInfo *ConnectionInfo, q Question, nameServer *NameServer, rootCAs *x509.CertPool, shouldVerifyServerCert, recursive bool, ednsOptions []dns.EDNS0, dnssec bool, checkingDisabled bool) (*SingleQueryResult, *dns.Msg, Status, error) { @@ -897,7 +893,8 @@ func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *Question nameServers := make([]NameServer, 0, len(result.Authorities)) for i, elem := range result.Authorities { if _, ok := elem.(DSAnswer); ok { - // DS records are not nameservers, skip them + // DS records are not nameservers but may present in the section + // https://datatracker.ietf.org/doc/html/rfc4035#section-3.1.4 continue } @@ -1011,7 +1008,7 @@ func (r *Resolver) extractAuthority(ctx context.Context, authority interface{}, return nil, StatusServFail, layer, trace } -func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, q Question, nameServer *NameServer, isIterative bool, depth int) (bool, error) { +func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, q Question, nameServer *NameServer, isIterative bool, depth int, trace Trace) (bool, Trace, error) { typeToRRSets := make(map[uint16][]dns.RR) typeToRRSigs := make(map[uint16][]*dns.RRSIG) @@ -1030,17 +1027,17 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, if len(typeToRRSigs) == 0 { // No RRSIGs, possibly because DNSSEC is not enabled... or we are hijacked r.verboseLog(depth+1, "DNSSEC: No RRSIGs found") - return false, nil + return false, trace, nil } if len(typeToRRSets) != len(typeToRRSigs) { - return false, errors.New("mismatched number of RRsets and RRSIGs") + return false, trace, errors.New("mismatched number of RRsets and RRSIGs") } // Verify if for each RRset there is a corresponding RRSIG for rrType := range typeToRRSets { if _, ok := typeToRRSigs[rrType]; !ok { - return false, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) + return false, trace, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) } } @@ -1056,23 +1053,23 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, } else if dnskey.Flags == zoneSigningKeyFlag { continue } else { - return false, fmt.Errorf("unknown DNSKEY flag: %d", dnskey.Flags) + return false, trace, fmt.Errorf("unknown DNSKEY flag: %d", dnskey.Flags) } } if ksk == nil { - return false, errors.New("could not find KSK") + return false, trace, errors.New("could not find KSK") } // Verify the RRSIGs for the DNSKEY RRset // TODO: key tag? if err := typeToRRSigs[dns.TypeDNSKEY][0].Verify(ksk, typeToRRSets[dns.TypeDNSKEY]); err != nil { - return false, errors.Wrap(err, "could not verify DNSKEY RRSIG") + return false, trace, errors.Wrap(err, "could not verify DNSKEY RRSIG") } // TODO: we'll just trust KSK for now (but we should find DS record and verify it) - return true, nil + return true, trace, nil } // Gather DNSKEYs @@ -1087,9 +1084,9 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, }, RetriesRemaining: &retries, } - res, _, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative) + res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace, depth+1) if status != StatusNoError || err != nil { - return false, fmt.Errorf("cannot get DNSKEYs, status: %s, err: %v", status, err) + return false, trace, fmt.Errorf("cannot get DNSKEYs, status: %s, err: %v", status, err) } // Type converting ZSK @@ -1120,11 +1117,11 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, // TODO should make use of sig.KeyTag. r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Verifying RRSIG for type %s", dns.TypeToString[rrType])) if err := sig.Verify(zsk, rrSet); err != nil { - return false, errors.Wrapf(err, "could not verify RRSIG for type %s", dns.TypeToString[rrType]) + return false, trace, errors.Wrapf(err, "could not verify RRSIG for type %s", dns.TypeToString[rrType]) } } - return true, nil + return true, trace, nil } // CheckTxtRecords common function for all modules based on search in TXT record From f45eab2de52de85b05773a81d4d4828878c65aab Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Wed, 30 Oct 2024 01:47:58 -0700 Subject: [PATCH 05/12] refactor: more comprehensive validation of DNSKEY/RRSIG dnssec related functions now sit in their own file --- src/zdns/answers.go | 61 ++++++++++ src/zdns/dnssec.go | 270 ++++++++++++++++++++++++++++++++++++++++++++ src/zdns/lookup.go | 125 +------------------- src/zdns/util.go | 3 + 4 files changed, 336 insertions(+), 123 deletions(-) create mode 100644 src/zdns/dnssec.go diff --git a/src/zdns/answers.go b/src/zdns/answers.go index 45ac2c54..5dffc63a 100644 --- a/src/zdns/answers.go +++ b/src/zdns/answers.go @@ -78,6 +78,21 @@ type DNSKEYAnswer struct { PublicKey string `json:"public_key" groups:"short,normal,long,trace"` } +func (r *DNSKEYAnswer) ToVanillaType() *dns.DNSKEY { + return &dns.DNSKEY{ + Hdr: dns.RR_Header{ + Name: dns.CanonicalName(r.Name), + Rrtype: r.RrType, + Class: dns.StringToClass[r.Class], + Ttl: r.TTL, + }, + Flags: r.Flags, + Protocol: r.Protocol, + Algorithm: r.Algorithm, + PublicKey: r.PublicKey, + } +} + type DSAnswer struct { Answer KeyTag uint16 `json:"key_tag" groups:"short,normal,long,trace"` @@ -86,6 +101,22 @@ type DSAnswer struct { Digest string `json:"digest" groups:"short,normal,long,trace"` } +func (r *DSAnswer) ToVanillaType() *dns.DS { + return &dns.DS{ + Hdr: dns.RR_Header{ + + Name: dns.CanonicalName(r.Name), + Rrtype: r.RrType, + Class: dns.StringToClass[r.Class], + Ttl: r.TTL, + }, + KeyTag: r.KeyTag, + Algorithm: r.Algorithm, + DigestType: r.DigestType, + Digest: r.Digest, + } +} + type GPOSAnswer struct { Answer Longitude string `json:"preference" groups:"short,normal,long,trace"` @@ -186,6 +217,36 @@ type RRSIGAnswer struct { Signature string `json:"signature" groups:"short,normal,long,trace"` } +func (r *RRSIGAnswer) ToVanillaType() *dns.RRSIG { + expiration, err := dns.StringToTime(r.Expiration) + if err != nil { + panic(fmt.Sprintf("failed to parse expiration time: %s", r.Expiration)) + } + + inception, err := dns.StringToTime(r.Inception) + if err != nil { + panic(fmt.Sprintf("failed to parse inception time: %s", r.Inception)) + } + + return &dns.RRSIG{ + Hdr: dns.RR_Header{ + Name: dns.CanonicalName(r.Name), + Rrtype: r.RrType, + Class: dns.StringToClass[r.Class], + Ttl: r.TTL, + }, + TypeCovered: r.TypeCovered, + Algorithm: r.Algorithm, + Labels: r.Labels, + OrigTtl: r.OriginalTTL, + Expiration: expiration, + Inception: inception, + KeyTag: r.KeyTag, + SignerName: r.SignerName, + Signature: r.Signature, + } +} + type RPAnswer struct { Answer Mbox string `json:"mbox" groups:"short,normal,long,trace"` diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go new file mode 100644 index 00000000..cf618198 --- /dev/null +++ b/src/zdns/dnssec.go @@ -0,0 +1,270 @@ +/* + * ZDNS Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package zdns + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/zmap/dns" +) + +const ( + zoneSigningKeyFlag = 256 + keySigningKeyFlag = 257 +) + +func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, q Question, nameServer *NameServer, isIterative bool, depth int, trace Trace) (bool, Trace, error) { + typeToRRSets := make(map[uint16][]dns.RR) + typeToRRSigs := make(map[uint16][]*dns.RRSIG) + + // Extract all the RRSets from the message + for _, rr := range msg.Answer { + rrType := rr.Header().Rrtype + if rrType == dns.TypeRRSIG { + rrSig := rr.(*dns.RRSIG) + typeToRRSigs[rrSig.TypeCovered] = append(typeToRRSigs[rrSig.TypeCovered], rrSig) + } else { + typeToRRSets[rrType] = append(typeToRRSets[rrType], rr) + } + } + + // Shortcut checks on RRSIG cardinality + if len(typeToRRSigs) == 0 { + // No RRSIGs, possibly because DNSSEC is not enabled... or we are hijacked + r.verboseLog(depth+1, "DNSSEC: No RRSIGs found") + return false, trace, nil + } + + if len(typeToRRSets) != len(typeToRRSigs) { + return false, trace, errors.New("mismatched number of RRsets and RRSIGs") + } + + // Verify if for each RRset there is a corresponding RRSIG + for rrType := range typeToRRSets { + if _, ok := typeToRRSigs[rrType]; !ok { + return false, trace, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) + } + } + + r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found %d RRsets and %d RRSIGs", len(typeToRRSets), len(typeToRRSigs))) + + passed, trace, err := r.validateRRSIGs(ctx, typeToRRSets, typeToRRSigs, nameServer, isIterative, trace, depth) + if err != nil { + return false, trace, errors.Wrap(err, "could not validate RRSIGs") + } + + return passed, trace, nil +} + +// parseKSKsFromAnswer extracts only KSKs (Key Signing Keys) from a DNSKEY RRset answer, +// populating a map where the KeyTag is the key and the DNSKEY is the value. +// This function skips ZSKs and returns an error if any unexpected flags or types are encountered. +// +// Parameters: +// - rrSet: The DNSKEY RRset answer to parse. +// +// Returns: +// - map[uint16]*dns.DNSKEY: A map of KeyTag to KSKs. +// - error: An error if an unexpected flag or type is encountered. +func parseKSKsFromAnswer(rrSet []dns.RR) (map[uint16]*dns.DNSKEY, error) { + ksks := make(map[uint16]*dns.DNSKEY) + + for _, rr := range rrSet { + dnskey, ok := rr.(*dns.DNSKEY) + if !ok { + return nil, fmt.Errorf("invalid RR type in DNSKEY RRset: %v", rr) + } + switch dnskey.Flags { + case keySigningKeyFlag: + ksks[dnskey.KeyTag()] = dnskey + case zoneSigningKeyFlag: + // Skip ZSKs + continue + default: + return nil, fmt.Errorf("unexpected DNSKEY flag: %d", dnskey.Flags) + } + } + + if len(ksks) == 0 { + return nil, errors.New("could not find any KSK in DNSKEY RRset") + } + + return ksks, nil +} + +// getDNSKEYs retrieves and separates KSKs and ZSKs from the signer domain's DNSKEYs, +// returning maps of KeyTags to DNSKEYs for both KSKs and ZSKs. +// +// Parameters: +// - ctx: Context for cancellation and timeout control. +// - signerDomain: The signer domain extracted from the RRSIG's SignerName field. +// - nameServer: The nameserver to use for the DNS query. +// - isIterative: Boolean indicating if the query should be iterative. +// - trace: The trace context for tracking the request path. +// - depth: The recursion or verification depth for logging purposes. +// +// Returns: +// - ksks: Map of KeyTag to KSKs (Key Signing Keys) retrieved from the signer domain. +// - zsks: Map of KeyTag to ZSKs (Zone Signing Keys) retrieved from the signer domain. +// - Trace: Updated trace context with the DNSKEY query included. +// - error: If the DNSKEY query fails or returns an unexpected status. +func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServer *NameServer, isIterative bool, trace Trace, depth int) (map[uint16]*dns.DNSKEY, map[uint16]*dns.DNSKEY, Trace, error) { + ksks := make(map[uint16]*dns.DNSKEY) + zsks := make(map[uint16]*dns.DNSKEY) + + retries := r.retries + nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), ".") + dnskeyQuestion := QuestionWithMetadata{ + Q: Question{ + Name: nameWithoutTrailingDot, + Type: dns.TypeDNSKEY, + Class: dns.ClassINET, + }, + RetriesRemaining: &retries, + } + + res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace, depth) + if status != StatusNoError || err != nil { + return nil, nil, trace, fmt.Errorf("cannot get DNSKEYs for signer domain %s, status: %s, err: %v", signerDomain, status, err) + } + + // RRSIGs of res should have been verified before returning to here. + + // Separate DNSKEYs into KSKs and ZSKs maps based on flags + for _, rr := range res.Answers { + zTypedKey, ok := rr.(DNSKEYAnswer) + if !ok { + r.verboseLog(depth, fmt.Sprintf("DNSSEC: Non-DNSKEY RR type in DNSKEY answer: %v", rr)) + continue + } + dnskey := zTypedKey.ToVanillaType() + + switch dnskey.Flags { + case keySigningKeyFlag: + ksks[dnskey.KeyTag()] = dnskey + case zoneSigningKeyFlag: + zsks[dnskey.KeyTag()] = dnskey + default: + r.verboseLog(depth, fmt.Sprintf("DNSSEC: Unexpected DNSKEY flag %d in DNSKEY answer", dnskey.Flags)) + } + } + + // Error if no KSK/ZSK is found + if len(ksks) == 0 || len(zsks) == 0 { + return nil, nil, trace, errors.New("missing at least one KSK or ZSK in DNSKEY answer") + } + + // TODO: Validate KSKs with DS records + + return ksks, zsks, trace, nil +} + +// validateRRSIG verifies multiple RRSIGs for a given RRset. For each RRSIG, it retrieves the necessary +// DNSKEYs (KSKs for DNSKEY RRsets, ZSKs for others) from either the answer directly (for DNSKEY types) or +// by querying the signer domain. Each RRSIG is validated only with the DNSKEY matching its KeyTag. +// +// Parameters: +// - ctx: Context for cancellation and timeout control. +// - rrSetType: The type of the RRset (e.g., dns.TypeA, dns.TypeDNSKEY). +// - rrSet: The RRset that is being verified. +// - rrsigs: A slice of RRSIGs associated with the RRset. +// - nameServer: The nameserver to use for DNSKEY retrievals. +// - isIterative: Boolean indicating if the DNSKEY queries should be iterative. +// - trace: The trace context for tracking the request path. +// - depth: The recursion or verification depth for logging purposes. +// +// Returns: +// - bool: Returns true if at least one RRSIG is successfully verified for the RRset. +// - Trace: Updated trace context including the DNSKEY retrievals and verifications. +// - error: If no RRSIG is verified, returns an error describing the failure. +func (r *Resolver) validateRRSIG(ctx context.Context, rrSetType uint16, rrSet []dns.RR, rrsigs []*dns.RRSIG, nameServer *NameServer, isIterative bool, trace Trace, depth int) (bool, Trace, error) { + var dnskeyMap map[uint16]*dns.DNSKEY + var err error + + // If RRset type is DNSKEY, use pre-parsed KSKs from the answer directly + if rrSetType == dns.TypeDNSKEY { + dnskeyMap, err = parseKSKsFromAnswer(rrSet) + if err != nil { + return false, trace, fmt.Errorf("failed to parse KSKs from DNSKEY answer: %v", err) + } + } else { + // For other RRset types, fetch DNSKEYs for each RRSIG's signer domain + for _, rrsig := range rrsigs { + r.verboseLog(depth, fmt.Sprintf("DNSSEC: Verifying RRSIG with signer %s", rrsig.SignerName)) + + _, zskMap, updatedTrace, err := r.getDNSKEYs(ctx, rrsig.SignerName, nameServer, isIterative, trace, depth+1) + dnskeyMap = zskMap + if err != nil { + return false, updatedTrace, fmt.Errorf("failed to retrieve DNSKEYs for signer domain %s: %v", rrsig.SignerName, err) + } + trace = updatedTrace + } + } + + // Attempt to verify each RRSIG using only the DNSKEY matching its KeyTag + for _, rrsig := range rrsigs { + keyTag := rrsig.KeyTag + matchingKey, found := dnskeyMap[keyTag] + if !found { + return false, trace, fmt.Errorf("no matching DNSKEY found for RRSIG with key tag %d", keyTag) + } + + // Verify the RRSIG with the matching DNSKEY + if err := rrsig.Verify(matchingKey, rrSet); err == nil { + return true, trace, nil + } + r.verboseLog(depth, fmt.Sprintf("DNSSEC: RRSIG with keytag=%d failed to verify", keyTag)) + } + + return false, trace, fmt.Errorf("could not verify any RRSIG for RRset") +} + +// validateRRSIGs verifies multiple RRsets and their corresponding RRSIGs. It iterates over each RRset, retrieves +// the RRSIGs, and attempts to verify each RRSIG using validateRRSIG, using KSKs for DNSKEY RRset type and ZSKs +// for other types. +// +// Parameters: +// - ctx: Context for cancellation and timeout control. +// - typeToRRSets: A map of RR types to slices of DNS Resource Records (RRsets) of that type. +// - typeToRRSigs: A map of RR types to slices of RRSIGs associated with each RRset. +// - nameServer: The nameserver to use for DNSKEY retrievals. +// - isIterative: Boolean indicating if the DNSKEY queries should be iterative. +// - depth: The recursion or verification depth for logging purposes. +// +// Returns: +// - bool: Returns true if all RRSIGs for all RRsets are verified; otherwise, false if any RRSIG fails verification. +// - Trace: Updated trace context with all verification attempts included. +// - error: If verification fails for any RRSIG, returns an error with details. +func (r *Resolver) validateRRSIGs(ctx context.Context, typeToRRSets map[uint16][]dns.RR, typeToRRSigs map[uint16][]*dns.RRSIG, nameServer *NameServer, isIterative bool, trace Trace, depth int) (bool, Trace, error) { + for rrType, rrSet := range typeToRRSets { + rrsigs, ok := typeToRRSigs[rrType] + if !ok || len(rrsigs) == 0 { + return false, trace, fmt.Errorf("no RRSIG found for type %s", dns.TypeToString[rrType]) + } + + r.verboseLog(depth, fmt.Sprintf("DNSSEC: Verifying RRSIGs for type %s", dns.TypeToString[rrType])) + + // Validate the RRSIGs for the RRset using validateRRSIG + passed, updatedTrace, err := r.validateRRSIG(ctx, rrType, rrSet, rrsigs, nameServer, isIterative, trace, depth+1) + trace = updatedTrace + if !passed { + return false, trace, fmt.Errorf("could not verify any RRSIG for type %s: %v", dns.TypeToString[rrType], err) + } + } + + return true, trace, nil +} diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index f1c53471..1ba3a672 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -33,11 +33,6 @@ import ( "github.com/zmap/zdns/src/internal/util" ) -const ( - zoneSigningKeyFlag = 256 - keySigningKeyFlag = 257 -) - // GetDNSServers returns a list of IPv4, IPv6 DNS servers from a file, or an error if one occurs func GetDNSServers(path string) (ipv4, ipv6 []string, err error) { c, err := dns.ClientConfigFromFile(path) @@ -424,7 +419,7 @@ func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMet r.verboseLog(depth+1, "Cycling lookup failed - out of retries. Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) return result, isCached, status, trace, errors.New("cycling lookup failed - out of retries") } - r.verboseLog(depth+1, "Cycling lookup failed, using a retry. Retries remaining: ", qWithMeta.RetriesRemaining, " , Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) + r.verboseLog(depth+1, "Cycling lookup failed with status:", status, "err: ", err, ", using a retry. Retries remaining: ", *qWithMeta.RetriesRemaining, " , Name: ", qWithMeta.Q.Name, ", Layer: ", layer, ", Nameserver: ", nameServer) *qWithMeta.RetriesRemaining-- } return &SingleQueryResult{}, false, StatusError, trace, errors.New("cycling lookup function did not exit properly") @@ -567,7 +562,7 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam if err != nil { return &SingleQueryResult{}, isCached, status, trace, errors.Wrap(err, "could not perform lookup") } - r.verboseLog(depth+2, "Results from wire for name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer, " status: ", status, " , err: ", err, " result: ", result) + r.verboseLog(depth+2, "Results from wire for name: ", q, ", Layer: ", layer, ", Nameserver: ", nameServer, " status: ", status, " , err: ", err, " result: ", *result) if status == StatusNoError && result != nil { if r.dnsSecEnabled { @@ -1008,122 +1003,6 @@ func (r *Resolver) extractAuthority(ctx context.Context, authority interface{}, return nil, StatusServFail, layer, trace } -func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, q Question, nameServer *NameServer, isIterative bool, depth int, trace Trace) (bool, Trace, error) { - typeToRRSets := make(map[uint16][]dns.RR) - typeToRRSigs := make(map[uint16][]*dns.RRSIG) - - // Extract all the RRSets from the message - for _, rr := range msg.Answer { - rrType := rr.Header().Rrtype - if rrType == dns.TypeRRSIG { - rrSig := rr.(*dns.RRSIG) - typeToRRSigs[rrSig.TypeCovered] = append(typeToRRSigs[rrSig.TypeCovered], rrSig) - } else { - typeToRRSets[rrType] = append(typeToRRSets[rrType], rr) - } - } - - // Shortcut checks on RRSIG cardinality - if len(typeToRRSigs) == 0 { - // No RRSIGs, possibly because DNSSEC is not enabled... or we are hijacked - r.verboseLog(depth+1, "DNSSEC: No RRSIGs found") - return false, trace, nil - } - - if len(typeToRRSets) != len(typeToRRSigs) { - return false, trace, errors.New("mismatched number of RRsets and RRSIGs") - } - - // Verify if for each RRset there is a corresponding RRSIG - for rrType := range typeToRRSets { - if _, ok := typeToRRSigs[rrType]; !ok { - return false, trace, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) - } - } - - r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found %d RRsets and %d RRSIGs", len(typeToRRSets), len(typeToRRSigs))) - - if q.Type == dns.TypeDNSKEY { - // Find Key Signing Key (KSK) and Zone Signing Key (ZSK) - var ksk *dns.DNSKEY - for _, rr := range typeToRRSets[dns.TypeDNSKEY] { - dnskey := rr.(*dns.DNSKEY) - if dnskey.Flags == keySigningKeyFlag { - ksk = dnskey - } else if dnskey.Flags == zoneSigningKeyFlag { - continue - } else { - return false, trace, fmt.Errorf("unknown DNSKEY flag: %d", dnskey.Flags) - } - } - - if ksk == nil { - return false, trace, errors.New("could not find KSK") - } - - // Verify the RRSIGs for the DNSKEY RRset - // TODO: key tag? - if err := typeToRRSigs[dns.TypeDNSKEY][0].Verify(ksk, typeToRRSets[dns.TypeDNSKEY]); err != nil { - return false, trace, errors.Wrap(err, "could not verify DNSKEY RRSIG") - } - - // TODO: we'll just trust KSK for now (but we should find DS record and verify it) - - return true, trace, nil - } - - // Gather DNSKEYs - // TODO: > 1 ZSK? - var zsk *dns.DNSKEY - retries := r.retries - dnskeyQuestion := QuestionWithMetadata{ - Q: Question{ - Name: q.Name, - Type: dns.TypeDNSKEY, - Class: q.Class, - }, - RetriesRemaining: &retries, - } - res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace, depth+1) - if status != StatusNoError || err != nil { - return false, trace, fmt.Errorf("cannot get DNSKEYs, status: %s, err: %v", status, err) - } - - // Type converting ZSK - for _, rr := range res.Answers { - switch rr := rr.(type) { - case DNSKEYAnswer: - // We don't care about KSK here. It must have been validated during DNSKEY lookup. - if rr.Flags == 256 { // TODO: magic number - zsk = &dns.DNSKEY{ - Hdr: dns.RR_Header{Name: dns.CanonicalName(rr.Name), Rrtype: rr.RrType, Class: dns.StringToClass[rr.Class], Ttl: rr.TTL}, - Flags: rr.Flags, - Protocol: rr.Protocol, - Algorithm: rr.Algorithm, - PublicKey: rr.PublicKey, - } - } - case RRSIGAnswer: - continue // It's normal to have RRSIGs in any DNSSEC-enabled response. - default: - r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found unexpected RRset in DNSKEY answer: %v", rr)) - continue - } - } - - // Verify the RRSIGs for each RRset - for rrType, rrSet := range typeToRRSets { - sig := typeToRRSigs[rrType][0] - // TODO should make use of sig.KeyTag. - r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Verifying RRSIG for type %s", dns.TypeToString[rrType])) - if err := sig.Verify(zsk, rrSet); err != nil { - return false, trace, errors.Wrapf(err, "could not verify RRSIG for type %s", dns.TypeToString[rrType]) - } - } - - return true, trace, nil -} - // CheckTxtRecords common function for all modules based on search in TXT record func CheckTxtRecords(res *SingleQueryResult, status Status, regex *regexp.Regexp, err error) (string, Status, error) { if status != StatusNoError { diff --git a/src/zdns/util.go b/src/zdns/util.go index 8036da18..f1eb6008 100644 --- a/src/zdns/util.go +++ b/src/zdns/util.go @@ -29,6 +29,9 @@ import ( const ZDNSVersion = "1.1.0" func dotName(name string) string { + if strings.HasSuffix(name, ".") { + log.Fatal("name already has trailing dot") + } return strings.Join([]string{name, "."}, "") } From f276da1786f956263c612c6b12d8d32f75d66569 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:14:44 -0700 Subject: [PATCH 06/12] patch: disregard RRSIG in authorities --- src/zdns/lookup.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index 1ba3a672..2859e273 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -887,9 +887,10 @@ func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *Question newTrace := trace nameServers := make([]NameServer, 0, len(result.Authorities)) for i, elem := range result.Authorities { - if _, ok := elem.(DSAnswer); ok { - // DS records are not nameservers but may present in the section - // https://datatracker.ietf.org/doc/html/rfc4035#section-3.1.4 + // Skip DS and RRSIG records as they are not nameservers but may present in the section + // https://datatracker.ietf.org/doc/html/rfc4035#section-3.1.4 + switch elem.(type) { + case DSAnswer, RRSIGAnswer: continue } From bf9c00997097d2a090e52c216657193123cd9e2b Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:20:00 -0700 Subject: [PATCH 07/12] fix: remove depth parameter in lookup function revert depth change. depth can't be carried to subqueries --- src/zdns/dnssec.go | 2 +- src/zdns/lookup.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go index cf618198..5f8232b7 100644 --- a/src/zdns/dnssec.go +++ b/src/zdns/dnssec.go @@ -137,7 +137,7 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ RetriesRemaining: &retries, } - res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace, depth) + res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace) if status != StatusNoError || err != nil { return nil, nil, trace, fmt.Errorf("cannot get DNSKEYs for signer domain %s, status: %s, err: %v", signerDomain, status, err) } diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index 2859e273..a2d6aa80 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -108,7 +108,7 @@ func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIt return r.followingLookup(ctx, &questionWithMeta, nameServers, isIterative) } var trace Trace - res, trace, status, err := r.lookup(ctx, &questionWithMeta, nameServers, isIterative, trace, 1) + res, trace, status, err := r.lookup(ctx, &questionWithMeta, nameServers, isIterative, trace) if err != nil { return res, nil, status, fmt.Errorf("could not perform retrying lookup for name %v: %w", q.Name, err) } @@ -116,7 +116,7 @@ func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIt } // lookup performs a DNS lookup for a given question against a slice of interchangeable nameservers, taking care of iterative and external lookups -func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, isIterative bool, trace Trace, depth int) (*SingleQueryResult, Trace, Status, error) { +func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, isIterative bool, trace Trace) (*SingleQueryResult, Trace, Status, error) { var res *SingleQueryResult var isCached IsCached var status Status @@ -125,15 +125,15 @@ func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, return res, trace, StatusTimeout, nil } if isIterative { - r.verboseLog(depth, "MIEKG-IN: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") - res, trace, status, err = r.iterativeLookup(ctx, qWithMeta, nameServers, depth, ".", trace) - r.verboseLog(depth, "MIEKG-OUT: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, "): status: ", status, " , err: ", err) + r.verboseLog(1, "MIEKG-IN: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") + res, trace, status, err = r.iterativeLookup(ctx, qWithMeta, nameServers, 1, ".", trace) + r.verboseLog(1, "MIEKG-OUT: following iterative lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, "): status: ", status, " , err: ", err) } else { tries := 0 // external lookup - r.verboseLog(depth, "MIEKG-IN: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") - res, isCached, status, trace, err = r.cyclingLookup(ctx, qWithMeta, nameServers, qWithMeta.Q.Name, depth, true, trace) - r.verboseLog(depth, "MIEKG-OUT: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ") with ", tries, " attempts: status: ", status, " , err: ", err) + r.verboseLog(1, "MIEKG-IN: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ")") + res, isCached, status, trace, err = r.cyclingLookup(ctx, qWithMeta, nameServers, qWithMeta.Q.Name, 1, true, trace) + r.verboseLog(1, "MIEKG-OUT: following external lookup for ", qWithMeta.Q.Name, " (", qWithMeta.Q.Type, ") with ", tries, " attempts: status: ", status, " , err: ", err) var t TraceStep // TODO check for null res if res != nil { @@ -146,7 +146,7 @@ func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, t.DNSClass = qWithMeta.Q.Class t.Name = qWithMeta.Q.Name t.Layer = qWithMeta.Q.Name - t.Depth = depth + t.Depth = 1 t.Cached = isCached t.Try = tries trace = append(trace, t) @@ -175,7 +175,7 @@ func (r *Resolver) followingLookup(ctx context.Context, qWithMeta *QuestionWithM r.verboseLog(0, "MIEKG-IN: starting a C/DNAME following lookup for ", originalName, " (", qWithMeta.Q.Type, ")") for i := 0; i < r.maxDepth; i++ { qWithMeta.Q.Name = currName // update the question with the current name, this allows following CNAMEs - iterRes, trace, iterStatus, lookupErr := r.lookup(ctx, qWithMeta, nameServers, isIterative, trace, 1) + iterRes, trace, iterStatus, lookupErr := r.lookup(ctx, qWithMeta, nameServers, isIterative, trace) if iterStatus != StatusNoError || lookupErr != nil { if i == 0 { // only have 1 result to return From 38b23f67e372d97f051099aaae30f808f03be6d9 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:11:15 -0700 Subject: [PATCH 08/12] feat: implement DS verification --- go.mod | 3 +- go.sum | 2 ++ src/zdns/dnssec.go | 86 +++++++++++++++++++++++++++++++++++++++++++--- src/zdns/lookup.go | 3 +- src/zdns/util.go | 9 +++++ 5 files changed, 97 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 60906a3b..2c0fa2d5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zmap/zdns -go 1.20 +go 1.21.1 require ( github.com/hashicorp/go-version v1.7.0 @@ -10,6 +10,7 @@ require ( github.com/schollz/progressbar/v3 v3.15.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 + github.com/zmap/go-dns-root-anchors v0.0.0-20241013070941-7d9393cc3f64 github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6 diff --git a/go.sum b/go.sum index 063747b4..cebd7e53 100644 --- a/go.sum +++ b/go.sum @@ -269,6 +269,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zmap/dns v1.1.63-zdns1 h1:vaZIXXLZqjmZTGcpu9qPDcunqWfxeRMh0OwLiCxcjsI= github.com/zmap/dns v1.1.63-zdns1/go.mod h1:L50pXblXGxDFLaon9W4vGSfC1rGIcBL29sS7sNvNKuI= +github.com/zmap/go-dns-root-anchors v0.0.0-20241013070941-7d9393cc3f64 h1:nimnL754tetLIX/BtOp7lXk5H2/Jqo472MYBNhWTgAo= +github.com/zmap/go-dns-root-anchors v0.0.0-20241013070941-7d9393cc3f64/go.mod h1:AkYq/HWOOEFPx6YgVn2oYjaZjLw2R/HV5WUWfJrFQ6s= github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M= github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go index 5f8232b7..ab1dd3c7 100644 --- a/src/zdns/dnssec.go +++ b/src/zdns/dnssec.go @@ -18,10 +18,12 @@ import ( "fmt" "strings" + "github.com/miekg/dns" "github.com/pkg/errors" - "github.com/zmap/dns" + rootanchors "github.com/zmap/go-dns-root-anchors" ) +const rootZone = "." const ( zoneSigningKeyFlag = 256 keySigningKeyFlag = 257 @@ -127,7 +129,11 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ zsks := make(map[uint16]*dns.DNSKEY) retries := r.retries - nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), ".") + nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), rootZone) + if signerDomain == rootZone { + nameWithoutTrailingDot = rootZone + } + dnskeyQuestion := QuestionWithMetadata{ Q: Question{ Name: nameWithoutTrailingDot, @@ -137,7 +143,7 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ RetriesRemaining: &retries, } - res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, []NameServer{*nameServer}, isIterative, trace) + res, trace, status, err := r.lookup(ctx, &dnskeyQuestion, r.rootNameServers, isIterative, trace) if status != StatusNoError || err != nil { return nil, nil, trace, fmt.Errorf("cannot get DNSKEYs for signer domain %s, status: %s, err: %v", signerDomain, status, err) } @@ -168,11 +174,83 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ return nil, nil, trace, errors.New("missing at least one KSK or ZSK in DNSKEY answer") } - // TODO: Validate KSKs with DS records + // Validate KSKs with DS records + if valid, trace, err := r.validateDSRecords(ctx, signerDomain, ksks, nameServer, isIterative, trace, depth); !valid { + return nil, nil, trace, errors.Wrap(err, "DS validation failed") + } return ksks, zsks, trace, nil } +// validateDSRecords validates DS records against DNSKEY records. +// +// Parameters: +// - ctx: Context for cancellation and timeout control. +// - signerDomain: The signer domain to query for DS records. +// - dnskeyMap: A map of KeyTag to KSKs to validate against. +// - nameServer: The nameserver to use for the DNS query. +// - isIterative: Boolean indicating if the query should be iterative. +// - trace: The trace context for tracking the request path. +// - depth: The recursion or verification depth for logging purposes. +// +// Returns: +// - bool: Returns true if all DS records are valid; otherwise, false. +// - Trace: Updated trace context with the DS query included. +// - error: If validation fails for any DS record, returns an error with details. +func (r *Resolver) validateDSRecords(ctx context.Context, signerDomain string, dnskeyMap map[uint16]*dns.DNSKEY, nameServer *NameServer, isIterative bool, trace Trace, depth int) (bool, Trace, error) { + retries := r.retries + nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), rootZone) + + dsQuestion := QuestionWithMetadata{ + Q: Question{ + Name: nameWithoutTrailingDot, + Type: dns.TypeDS, + Class: dns.ClassINET, + }, + RetriesRemaining: &retries, + } + + dsRecords := make(map[uint16]dns.DS) + if signerDomain == rootZone { + // Root zone, use the root anchors + dsRecords = rootanchors.GetValidDSRecords() + } else { + res, trace, status, err := r.lookup(ctx, &dsQuestion, r.rootNameServers, isIterative, trace) + if status != StatusNoError || err != nil { + return false, trace, fmt.Errorf("cannot get DS records for signer domain %s, status: %s, err: %v", signerDomain, status, err) + } + + // RRSIGs of res should have been verified before returning to here. + + for _, rr := range res.Answers { + zTypedDS, ok := rr.(DSAnswer) + if !ok { + r.verboseLog(depth, fmt.Sprintf("DNSSEC: Non-DS RR type in DS answer: %v", rr)) + continue + } + ds := zTypedDS.ToVanillaType() + dsRecords[ds.KeyTag] = *ds + } + } + + for _, ksk := range dnskeyMap { + authenticDS, ok := dsRecords[ksk.KeyTag()] + if !ok { + return false, trace, fmt.Errorf("no DS record found for KSK with KeyTag %d", ksk.KeyTag()) + } + + actualDS := ksk.ToDS(authenticDS.DigestType) + actualDigest := strings.ToUpper(actualDS.Digest) + authenticDigest := strings.ToUpper(authenticDS.Digest) + if actualDigest != authenticDigest { + r.verboseLog(depth, fmt.Sprintf("DNSSEC: DS record mismatch for KSK with KeyTag %d: expected %s, got %s", ksk.KeyTag(), authenticDigest, actualDigest)) + return false, trace, fmt.Errorf("DS record mismatch for KSK with KeyTag %d", ksk.KeyTag()) + } + } + + return true, trace, nil +} + // validateRRSIG verifies multiple RRSIGs for a given RRset. For each RRSIG, it retrieves the necessary // DNSKEYs (KSKs for DNSKEY RRsets, ZSKs for others) from either the answer directly (for DNSKEY types) or // by querying the signer domain. Each RRSIG is validated only with the DNSKEY matching its KeyTag. diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index a2d6aa80..b6563f51 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -505,7 +505,8 @@ func (r *Resolver) cachedLookup(ctx context.Context, q Question, nameServer *Nam r.verboseLog(depth+2, err) return &SingleQueryResult{}, isCached, StatusAuthFail, trace, errors.Wrap(err, "could not get next authority with name: "+name+" and layer: "+layer) } - if name != layer && authName != layer { + // DS records are special, we need to query the parent zone and therefore cannot use the cache + if name != layer && authName != layer && q.Type != dns.TypeDS { // we have a valid authority to check the cache for if authName == "" { r.verboseLog(depth+2, "Can't parse name to authority properly. name: ", name, ", layer: ", layer) diff --git a/src/zdns/util.go b/src/zdns/util.go index f1eb6008..7a48b712 100644 --- a/src/zdns/util.go +++ b/src/zdns/util.go @@ -29,9 +29,14 @@ import ( const ZDNSVersion = "1.1.0" func dotName(name string) string { + if name == "." { + return name + } + if strings.HasSuffix(name, ".") { log.Fatal("name already has trailing dot") } + return strings.Join([]string{name, "."}, "") } @@ -123,6 +128,10 @@ func nextAuthority(name, layer string) (string, error) { return "in-addr.arpa", nil } + if name == "." && layer == "." { + return ".", nil + } + idx := strings.LastIndex(name, ".") if idx < 0 || (idx+1) >= len(name) { return name, nil From a922e1cecee3cd4e136f41a34c749f3d89fbff2c Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Fri, 1 Nov 2024 02:51:16 -0700 Subject: [PATCH 09/12] feat: cache DS in referrals --- src/zdns/cache.go | 26 ++++++++++++++++++++++++++ src/zdns/dnssec.go | 4 ++-- src/zdns/util.go | 7 +++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/zdns/cache.go b/src/zdns/cache.go index d88fca54..6a222b32 100644 --- a/src/zdns/cache.go +++ b/src/zdns/cache.go @@ -309,6 +309,32 @@ func (s *Cache) SafeAddCachedAuthority(res *SingleQueryResult, ns *NameServer, d nsString = ns.String() } + // Referrals may contain DS records in the authority section. These need to be cached under the child name. + var dsRRs []interface{} + var otherRRs []interface{} + for _, rr := range res.Authorities { + if dsRR, ok := rr.(DSAnswer); ok { + dsRRs = append(dsRRs, dsRR) + } else { + otherRRs = append(otherRRs, rr) + } + } + + if len(dsRRs) > 0 { + dsRes := &SingleQueryResult{ + Answers: dsRRs, + Protocol: res.Protocol, + Resolver: res.Resolver, + Flags: res.Flags, + TLSServerHandshake: res.TLSServerHandshake, + } + dsRes.Flags.Authoritative = true + delegateName := removeTrailingDotIfNotRoot(dsRRs[0].(DSAnswer).BaseAns().Name) + dsCachedRes := s.buildCachedResult(dsRes, depth, layer) + s.addCachedAnswer(Question{Name: delegateName, Type: dns.TypeDS, Class: dns.ClassINET}, nsString, false, dsCachedRes, depth) + } + + res.Authorities = otherRRs cachedRes := s.buildCachedResult(res, depth, layer) if len(cachedRes.Answers) == 0 && len(cachedRes.Authorities) == 0 && len(cachedRes.Additionals) == 0 { s.VerboseLog(depth+1, "SafeAddCachedAnswer: no cacheable records found, aborting") diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go index ab1dd3c7..80c111de 100644 --- a/src/zdns/dnssec.go +++ b/src/zdns/dnssec.go @@ -129,7 +129,7 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ zsks := make(map[uint16]*dns.DNSKEY) retries := r.retries - nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), rootZone) + nameWithoutTrailingDot := removeTrailingDotIfNotRoot(signerDomain) if signerDomain == rootZone { nameWithoutTrailingDot = rootZone } @@ -199,7 +199,7 @@ func (r *Resolver) getDNSKEYs(ctx context.Context, signerDomain string, nameServ // - error: If validation fails for any DS record, returns an error with details. func (r *Resolver) validateDSRecords(ctx context.Context, signerDomain string, dnskeyMap map[uint16]*dns.DNSKEY, nameServer *NameServer, isIterative bool, trace Trace, depth int) (bool, Trace, error) { retries := r.retries - nameWithoutTrailingDot := strings.TrimSuffix(dns.CanonicalName(signerDomain), rootZone) + nameWithoutTrailingDot := removeTrailingDotIfNotRoot(signerDomain) dsQuestion := QuestionWithMetadata{ Q: Question{ diff --git a/src/zdns/util.go b/src/zdns/util.go index 7a48b712..a47a906f 100644 --- a/src/zdns/util.go +++ b/src/zdns/util.go @@ -40,6 +40,13 @@ func dotName(name string) string { return strings.Join([]string{name, "."}, "") } +func removeTrailingDotIfNotRoot(name string) string { + if name == "." { + return name + } + return strings.TrimSuffix(name, ".") +} + func TranslateMiekgErrorCode(err int) Status { return Status(dns.RcodeToString[err]) } From 2b69182f725ae7e98c0356c4b29a6bbc30389fef Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Fri, 1 Nov 2024 05:29:21 -0700 Subject: [PATCH 10/12] feat: validate dnssec for referrals --- src/zdns/dnssec.go | 48 +++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go index 80c111de..2059134f 100644 --- a/src/zdns/dnssec.go +++ b/src/zdns/dnssec.go @@ -33,15 +33,10 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, typeToRRSets := make(map[uint16][]dns.RR) typeToRRSigs := make(map[uint16][]*dns.RRSIG) - // Extract all the RRSets from the message - for _, rr := range msg.Answer { - rrType := rr.Header().Rrtype - if rrType == dns.TypeRRSIG { - rrSig := rr.(*dns.RRSIG) - typeToRRSigs[rrSig.TypeCovered] = append(typeToRRSigs[rrSig.TypeCovered], rrSig) - } else { - typeToRRSets[rrType] = append(typeToRRSets[rrType], rr) - } + if msg.Authoritative { + updateTypeMapWithRRs(typeToRRSets, typeToRRSigs, msg.Answer) + } else { + updateTypeMapWithRRs(typeToRRSets, typeToRRSigs, msg.Ns) } // Shortcut checks on RRSIG cardinality @@ -51,18 +46,20 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, return false, trace, nil } - if len(typeToRRSets) != len(typeToRRSigs) { - return false, trace, errors.New("mismatched number of RRsets and RRSIGs") - } - // Verify if for each RRset there is a corresponding RRSIG + typeToRRSetsWithRRSIGs := make(map[uint16][]dns.RR) for rrType := range typeToRRSets { if _, ok := typeToRRSigs[rrType]; !ok { - return false, trace, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) + if msg.Authoritative || isDNSSECRecordType(rrType) { + return false, trace, fmt.Errorf("found RRset for type %s but no RRSIG", dns.TypeToString[rrType]) + } + } else { + typeToRRSetsWithRRSIGs[rrType] = typeToRRSets[rrType] } } + typeToRRSets = typeToRRSetsWithRRSIGs - r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found %d RRsets and %d RRSIGs", len(typeToRRSets), len(typeToRRSigs))) + r.verboseLog(depth+1, fmt.Sprintf("DNSSEC: Found %d RRsets with RRSIGs", len(typeToRRSets))) passed, trace, err := r.validateRRSIGs(ctx, typeToRRSets, typeToRRSigs, nameServer, isIterative, trace, depth) if err != nil { @@ -72,6 +69,27 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, return passed, trace, nil } +func isDNSSECRecordType(rrType uint16) bool { + switch rrType { + case dns.TypeDNSKEY, dns.TypeRRSIG, dns.TypeDS, dns.TypeNSEC, dns.TypeNSEC3, dns.TypeNSEC3PARAM: + return true + default: + return false + } +} + +func updateTypeMapWithRRs(typeToRRSets map[uint16][]dns.RR, typeToRRSigs map[uint16][]*dns.RRSIG, rrs []dns.RR) { + for _, rr := range rrs { + rrType := rr.Header().Rrtype + if rrType == dns.TypeRRSIG { + rrSig := rr.(*dns.RRSIG) + typeToRRSigs[rrSig.TypeCovered] = append(typeToRRSigs[rrSig.TypeCovered], rrSig) + } else { + typeToRRSets[rrType] = append(typeToRRSets[rrType], rr) + } + } +} + // parseKSKsFromAnswer extracts only KSKs (Key Signing Keys) from a DNSKEY RRset answer, // populating a map where the KeyTag is the key and the DNSKEY is the value. // This function skips ZSKs and returns an error if any unexpected flags or types are encountered. From ca5a2b5fa0af8964fbb978cf874587362f188e6f Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Fri, 1 Nov 2024 05:41:23 -0700 Subject: [PATCH 11/12] build(ci): bump to go1.21 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a52ccbc7..d675a5e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: "1.21" - name: Build run: | go version @@ -46,7 +46,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: "1.23" - name: Other lint run: | go install golang.org/x/tools/cmd/goimports@latest From 4d62421ccb0cc1225ba81c9f0d36f6991aa69652 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:55:56 -0700 Subject: [PATCH 12/12] fix: answer section is always validated --- src/zdns/dnssec.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/zdns/dnssec.go b/src/zdns/dnssec.go index 2059134f..f336013f 100644 --- a/src/zdns/dnssec.go +++ b/src/zdns/dnssec.go @@ -33,9 +33,8 @@ func (r *Resolver) validateChainOfDNSSECTrust(ctx context.Context, msg *dns.Msg, typeToRRSets := make(map[uint16][]dns.RR) typeToRRSigs := make(map[uint16][]*dns.RRSIG) - if msg.Authoritative { - updateTypeMapWithRRs(typeToRRSets, typeToRRSigs, msg.Answer) - } else { + updateTypeMapWithRRs(typeToRRSets, typeToRRSigs, msg.Answer) + if !msg.Authoritative && isIterative { updateTypeMapWithRRs(typeToRRSets, typeToRRSigs, msg.Ns) }