Skip to content

Commit

Permalink
feat: DNSSEC Validation (#470)
Browse files Browse the repository at this point in the history
* refactor: return raw dns response from auxiliary functions

* feat: basic, same-level DNSSEC validation

* fix: newLayer should not be updated if extractAuthority fails

* refactor: extend trace, fix depth for couple places

* refactor: more comprehensive validation of DNSKEY/RRSIG

dnssec related functions now sit in their own file

* patch: disregard RRSIG in authorities

* fix: remove depth parameter in lookup function

revert depth change. depth can't be carried to subqueries

* feat: implement DS verification

* feat: cache DS in referrals

* feat: validate dnssec for referrals

* build(ci): bump to go1.21

* fix: answer section is always validated

* fix: resolve linter issues

* fix: resolve linter issues

* refactor: store current retry in resolver struct

* patch: don't modify original res

* docs: rrset validation failure

* build(deps): bump root anchors

* fix: add validity period check for RRSIG

* feat: add dnssec result types

* docs: clarify comments for retries and retriesRemaining in Resolver

* feat: dnssec result field

* refactor: simplify param list with validator struct

* refactor: extended DNSSEC result handling

* feat: add JSON tags to DNSSEC types for improved serialization

* refactor: DNSSEC functions now does not stop resolution

* fix: regression in authority caching

* feat: add DNSSEC validation as CLI option

* feat: implement circular query detection

* chore: suppress some lint warnings

* fix: handle DNSSEC insecure and bogus statuses

* test: add DNSSEC integration tests

* fix: RRset should be identified by all of name, class and type

Have this super weird case where additionals from dnssec-tools.org contains an A and RRSIG for each of (nsm|nsw).dnssec-tools.org.
If identify by only type, these two will be clustered under the same set and could not validate.

* fix: dedup ds/dnskey

* docs: function comments for dnssec

* fix: shortcut Insecure if entire answer is unsigned

* feat: add dnssec output fields

* Revert "test: add DNSSEC integration tests"

This reverts commit 2b074a2.

* test: add DNSSEC integration tests

* fix: rrsig error handling and DS validation on DNSKEY response

* fix: KSK is a lie :(

* chore: error logging

* docs: comments

* fix: skip validation for some sections if answer is authoritative

* feat: lazy query A/AAAA of NSes when iterating on authorities

* fix: should restore the previous dnssec setting

* docs: add RFC references

* docs: add one rfc reference

* refactor: additionals -> additional

* refactor: passing on validation result from front of the chain

* refactor: miscellaneous bits

* fix: NSEC3 type definition

* feat: NSEC3 validation for DS records

* style: fix linter issues

* test: add integration test for dnssec with cnames

* fix: bogus case classification

* patch: comment

* feat: support NSEC for DS check

* fix: always cache under lower-cased authname

* fix: DS records should always be cached as secure

* fix: support DS/NSEC in authority section

* chore: logging for a specific nsec3 case

It seems cloudflare will not validate anything with iteration > 0. We are fine validating them, but print a log for visibility.

* revert change to go 1.21.1

* downgrade required go version to 1.20

* fix lint error

* toolchain directive not available in go 1.20

* upgrade -> go 1.21

---------

Co-authored-by: Zakir Durumeric <[email protected]>
Co-authored-by: phillip-stephens <[email protected]>
  • Loading branch information
3 people authored Dec 18, 2024
1 parent bf4aac1 commit 60afb38
Show file tree
Hide file tree
Showing 18 changed files with 1,249 additions and 158 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/large_scan_integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.20'
go-version: '1.21'
- name: Build
run: |
go version
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*.iml
*.code-workspace
/zdns
*.log
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ result verbosity levels: `short`, `normal` (default), `long`, and `trace`:
Users can also include specific additional fields using the `--include-fields`
flag and specifying a list of fields, e.g., `--include-fields=flags,resolver`.
Additional fields are: class, protocol, ttl, resolver, flags.
Additional fields are: class, protocol, ttl, resolver, flags, dnssec.
Name Server Mode
----------------
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/zmap/zdns

go 1.20
go 1.21

require (
github.com/hashicorp/go-version v1.7.0
Expand All @@ -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.10.0
github.com/zmap/go-dns-root-anchors v0.0.0-20241218192521-63aee68224b6
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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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-20241218192521-63aee68224b6 h1:vJV1O3ZIH9ywqPKaiYCHkyZOZmSZvdRHjlqmBk3JIIs=
github.com/zmap/go-dns-root-anchors v0.0.0-20241218192521-63aee68224b6/go.mod h1:cmGnqnZjrvWCBaJclyvXNP6yHEDjwoSV06GmM1zmdXU=
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=
Expand Down
3 changes: 2 additions & 1 deletion src/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type QueryOptions struct {
ClassString string `long:"class" default:"INET" description:"DNS class to query. Options: INET, CSNET, CHAOS, HESIOD, NONE, ANY."`
ClientSubnetString string `long:"client-subnet" description:"Client subnet in CIDR format for EDNS0."`
Dnssec bool `long:"dnssec" description:"Requests DNSSEC records by setting the DNSSEC OK (DO) bit"`
ValidateDNSSEC bool `long:"validate-dnssec" description:"Validate DNSSEC records, only applicable with --iterative"`
UseNSID bool `long:"nsid" description:"Request NSID."`
}

Expand All @@ -93,7 +94,7 @@ type InputOutputOptions struct {
BlacklistFilePath string `long:"blacklist-file" description:"blacklist file for servers to exclude from lookups"`
DNSConfigFilePath string `long:"conf-file" default:"/etc/resolv.conf" description:"config file for DNS servers"`
MultipleModuleConfigFilePath string `short:"c" long:"multi-config-file" description:"config file path for multiple module"`
IncludeInOutput string `long:"include-fields" description:"Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags"`
IncludeInOutput string `long:"include-fields" description:"Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags, dnssec"`
InputFilePath string `short:"f" long:"input-file" default:"-" description:"names to read, defaults to stdin"`
LogFilePath string `long:"log-file" default:"-" description:"where should JSON logs be saved, defaults to stderr"`
MetadataFilePath string `long:"metadata-file" description:"where should JSON metadata be saved, defaults to no metadata output. Use '-' for stderr."`
Expand Down
12 changes: 11 additions & 1 deletion src/cli/worker_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,17 @@ func populateResolverConfig(gc *CLIConf) *zdns.ResolverConfig {
config.MaxDepth = gc.MaxDepth
config.CheckingDisabledBit = gc.CheckingDisabled
config.ShouldRecycleSockets = !gc.DisableRecycleSockets
config.DNSSecEnabled = gc.Dnssec

config.ShouldValidateDNSSEC = gc.ValidateDNSSEC
if config.ShouldValidateDNSSEC {
config.DNSSecEnabled = true
if !gc.IterativeResolution {
log.Fatal("DNSSEC validation is only supported with iterative resolution")
}
} else {
config.DNSSecEnabled = gc.Dnssec
}

config.DNSConfigFilePath = gc.DNSConfigFilePath

config.LogLevel = log.Level(gc.Verbosity)
Expand Down
106 changes: 106 additions & 0 deletions src/zdns/answers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down Expand Up @@ -143,16 +174,50 @@ type NSECAnswer struct {
TypeBitMap string `json:"type_bit_map" groups:"short,normal,long,trace"`
}

func (r *NSECAnswer) ToVanillaType() *dns.NSEC {
return &dns.NSEC{
Hdr: dns.RR_Header{
Name: dns.CanonicalName(r.Name),
Rrtype: r.RrType,
Class: dns.StringToClass[r.Class],
Ttl: r.TTL,
},
NextDomain: r.NextDomain,
TypeBitMap: makeBitArray(r.TypeBitMap),
}
}

type NSEC3Answer struct {
Answer
HashAlgorithm uint8 `json:"hash_algorithm" groups:"short,normal,long,trace"`
Flags uint8 `json:"flags" groups:"short,normal,long,trace"`
Iterations uint16 `json:"iterations" groups:"short,normal,long,trace"`
SaltLength uint8 `json:"salt_length" groups:"short,normal,long,trace"`
Salt string `json:"salt" groups:"short,normal,long,trace"`
HashLength uint8 `json:"hash_length" groups:"short,normal,long,trace"`
NextDomain string `json:"next_domain" groups:"short,normal,long,trace"`
TypeBitMap string `json:"type_bit_map" groups:"short,normal,long,trace"`
}

func (r *NSEC3Answer) ToVanillaType() *dns.NSEC3 {
return &dns.NSEC3{
Hdr: dns.RR_Header{
Name: dns.CanonicalName(r.Name),
Rrtype: r.RrType,
Class: dns.StringToClass[r.Class],
Ttl: r.TTL,
},
Hash: r.HashAlgorithm,
Flags: r.Flags,
Iterations: r.Iterations,
SaltLength: uint8(len(r.Salt)),
Salt: r.Salt,
HashLength: r.HashLength,
NextDomain: r.NextDomain,
TypeBitMap: makeBitArray(r.TypeBitMap),
}
}

type NSEC3ParamAnswer struct {
Answer
HashAlgorithm uint8 `json:"hash_algorithm" groups:"short,normal,long,trace"`
Expand Down Expand Up @@ -186,6 +251,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("failed to parse expiration time: " + r.Expiration)
}

inception, err := dns.StringToTime(r.Inception)
if err != nil {
panic("failed to parse inception time: " + 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"`
Expand Down Expand Up @@ -324,6 +419,15 @@ func makeBitString(bm []uint16) string {
return retv
}

func makeBitArray(s string) []uint16 {
fields := strings.Fields(s)
retv := make([]uint16, 0, len(fields))
for _, t := range fields {
retv = append(retv, dns.StringToType[t])
}
return retv
}

func makeBaseAnswer(hdr *dns.RR_Header, answer string) Answer {
return Answer{
TTL: hdr.Ttl,
Expand Down Expand Up @@ -661,7 +765,9 @@ func ParseAnswer(ans dns.RR) interface{} {
HashAlgorithm: cAns.Hash,
Flags: cAns.Flags,
Iterations: cAns.Iterations,
SaltLength: cAns.SaltLength,
Salt: cAns.Salt,
HashLength: cAns.HashLength,
NextDomain: cAns.NextDomain,
TypeBitMap: makeBitString(cAns.TypeBitMap),
}
Expand Down
Loading

0 comments on commit 60afb38

Please sign in to comment.