-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
34 changed files
with
2,906 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package dnssec | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
// buildDelegationChain queries the RRs required for the zone validation. | ||
// It begins the queries at the root zone and then go down the delegation | ||
// chain until it reaches the desired zone, or an unsigned zone. | ||
// It returns a delegation chain of signed zones where the | ||
// first signed zone (index 0) is the root zone and the last signed | ||
// zone is the last signed zone, which can be the desired zone. | ||
func buildDelegationChain(handler dns.Handler, desiredZone string, qClass uint16) ( | ||
delegationChain []signedData, err error) { | ||
zoneNames := desiredZoneToZoneNames(desiredZone) | ||
delegationChain = make([]signedData, 0, len(zoneNames)) | ||
|
||
for _, zoneName := range zoneNames { | ||
// zoneName iterates in this order: ., com., example.com. | ||
data, signed, err := queryDelegation(handler, zoneName, qClass) | ||
if err != nil { | ||
return nil, fmt.Errorf("querying delegation for desired zone %s: %w", | ||
desiredZone, err) | ||
} | ||
delegationChain = append(delegationChain, data) | ||
if !signed { | ||
// first zone without a DS RRSet, but it should | ||
// have at least one NSEC or NSEC3 RRSet, even for | ||
// NXDOMAIN responses. | ||
break | ||
} | ||
} | ||
|
||
return delegationChain, nil | ||
} | ||
|
||
func desiredZoneToZoneNames(desiredZone string) (zoneNames []string) { | ||
if desiredZone == "." { | ||
return []string{"."} | ||
} | ||
|
||
zoneParts := strings.Split(desiredZone, ".") | ||
zoneNames = make([]string, len(zoneParts)) | ||
for i := range zoneParts { | ||
zoneNames[i] = dns.Fqdn(strings.Join(zoneParts[len(zoneParts)-1-i:], ".")) | ||
} | ||
return zoneNames | ||
} | ||
|
||
// queryDelegation obtains the DS RRSet and the DNSKEY RRSet | ||
// for a given zone and class, and creates a signed zone with | ||
// this information. It does not query the (non existent) | ||
// DS record for the root zone, which is the trust root anchor. | ||
func queryDelegation(handler dns.Handler, zone string, qClass uint16) ( | ||
data signedData, signed bool, err error) { | ||
data.zone = zone | ||
data.class = qClass | ||
|
||
// TODO set root zone DS here! | ||
|
||
// do not query DS for root zone since its DS record | ||
// is the trust root anchor. | ||
if zone != "." { | ||
data.dsResponse, err = queryDS(handler, zone, qClass) | ||
if err != nil { | ||
return signedData{}, false, fmt.Errorf("querying DS record: %w", err) | ||
} | ||
|
||
if data.dsResponse.isNoData() || data.dsResponse.isNXDomain() { | ||
// If no DS RRSet is found, the entire zone is unsigned. | ||
// This also means no DNSKEY RRSet exists, since child zones are | ||
// also unsigned, so return with the error errZoneHasNoDSRcord | ||
// to signal the caller to stop the delegation chain queries for | ||
// child zones when encountering a zone with no DS RRSet. | ||
return data, false, nil | ||
} | ||
} | ||
|
||
data.dnsKeyResponse, err = queryDNSKeys(handler, zone, qClass) | ||
if err != nil { | ||
return signedData{}, true, fmt.Errorf("querying DNSKEY record: %w", err) | ||
} | ||
|
||
return data, true, nil | ||
} | ||
|
||
var ( | ||
ErrDSAndNSECAbsent = errors.New("zone has no DS record and no NSEC record") | ||
) | ||
|
||
func queryDS(handler dns.Handler, zone string, qClass uint16) ( | ||
response dnssecResponse, err error) { | ||
response, err = queryRRSets(handler, zone, qClass, dns.TypeDS) | ||
switch { | ||
case err != nil: | ||
return dnssecResponse{}, err | ||
case !response.isSigned(): | ||
// no signed DS answer and no NSEC/NSEC3 authority RR | ||
return dnssecResponse{}, wrapError( | ||
zone, qClass, dns.TypeDS, ErrDSAndNSECAbsent) | ||
case response.isNXDomain(), response.isNoData(): | ||
// there is one or more NSEC/NSEC3 authority RRSets. | ||
return response, nil | ||
} | ||
// signed answer RRSet(s) | ||
|
||
// Double check we only have 1 DS RRSet. | ||
// TODO remove? | ||
err = dnssecRRSetsIsSingleOfType(response.answerRRSets, dns.TypeDS) | ||
if err != nil { | ||
return dnssecResponse{}, | ||
wrapError(zone, qClass, dns.TypeDS, err) | ||
} | ||
|
||
return response, nil | ||
} | ||
|
||
// queryDNSKeys queries the DNSKEY records for a given signed zone | ||
// containing a DS RRSet. It returns an error if the DNSKEY RRSet is | ||
// missing or is unsigned. | ||
// Note this returns all the DNSKey RRs, even non-zone ones. | ||
func queryDNSKeys(handler dns.Handler, qname string, qClass uint16) ( | ||
response dnssecResponse, err error) { | ||
// DNSKey RRSet(s) should be present so the NSEC/NSEC3 RRSet is ignored. | ||
response, err = queryRRSets(handler, qname, qClass, dns.TypeDNSKEY) | ||
switch { | ||
case err != nil: | ||
return dnssecResponse{}, err | ||
case !response.isSigned(), response.isNoData(): // cannot be NXDOMAIN | ||
// no signed DNSKEY answer | ||
return dnssecResponse{}, fmt.Errorf("for %s: %w", | ||
nameClassTypeToString(qname, qClass, dns.TypeDNSKEY), | ||
ErrDNSKeyNotFound) | ||
} | ||
|
||
// Double check we only have 1 DNSKEY RRSet. | ||
// TODO remove? | ||
err = dnssecRRSetsIsSingleOfType(response.answerRRSets, dns.TypeDNSKEY) | ||
if err != nil { | ||
return dnssecResponse{}, | ||
wrapError(qname, qClass, dns.TypeDNSKEY, err) | ||
} | ||
|
||
return response, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package dnssec | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_desiredZoneToZoneNames(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
desiredZone string | ||
zoneNames []string | ||
}{ | ||
"root": { | ||
desiredZone: ".", | ||
zoneNames: []string{"."}, | ||
}, | ||
"com": { | ||
desiredZone: "com.", | ||
zoneNames: []string{".", "com."}, | ||
}, | ||
"example.com": { | ||
desiredZone: "example.com.", | ||
zoneNames: []string{".", "com.", "example.com."}, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
testCase := testCase | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
zoneNames := desiredZoneToZoneNames(testCase.desiredZone) | ||
assert.Equal(t, testCase.zoneNames, zoneNames) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package dnssec | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
func mustRRToCNAME(rr dns.RR) *dns.CNAME { | ||
cname, ok := rr.(*dns.CNAME) | ||
if !ok { | ||
panic(fmt.Sprintf("RR is of type %T and not of type *dns.CNAME", rr)) | ||
} | ||
return cname | ||
} | ||
|
||
func getCnameTarget(rrSets []dnssecRRSet) (target string) { | ||
for _, rrSet := range rrSets { | ||
if rrSet.qtype() == dns.TypeCNAME { | ||
cname := mustRRToCNAME(rrSet.rrSet[0]) | ||
return cname.Target | ||
} | ||
} | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package dnssec | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
func mustRRToDNSKey(rr dns.RR) *dns.DNSKEY { | ||
dnsKey, ok := rr.(*dns.DNSKEY) | ||
if !ok { | ||
panic(fmt.Sprintf("RR is of type %T and not of type *dns.DNSKEY", rr)) | ||
} | ||
return dnsKey | ||
} | ||
|
||
// makeKeyTagToDNSKey creates a map of key tag to DNSKEY from a DNSKEY RRSet, | ||
// ignoring any RR which is not a Zone signing key. | ||
func makeKeyTagToDNSKey(dnsKeyRRSet []dns.RR) (keyTagToDNSKey map[uint16]*dns.DNSKEY) { | ||
keyTagToDNSKey = make(map[uint16]*dns.DNSKEY, len(dnsKeyRRSet)) | ||
for _, dnsKeyRR := range dnsKeyRRSet { | ||
dnsKey := mustRRToDNSKey(dnsKeyRR) | ||
if dnsKey.Flags&dns.ZONE == 0 { | ||
// As described in https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 | ||
// and https://datatracker.ietf.org/doc/html/rfc4034#section-5.2: | ||
// If bit 7 has value 0, then the DNSKEY record holds some other type of DNS | ||
// public key and MUST NOT be used to verify RRSIGs that cover RRsets. | ||
// The DNSKEY RR Flags MUST have Flags bit 7 set. If the | ||
// DNSKEY flags do not indicate a DNSSEC zone key, the DS | ||
// RR (and the DNSKEY RR it references) MUST NOT be used | ||
// in the validation process. | ||
continue | ||
} | ||
keyTagToDNSKey[dnsKey.KeyTag()] = dnsKey | ||
} | ||
return keyTagToDNSKey | ||
} | ||
|
||
const ( | ||
algoPreferenceRecommended uint8 = iota | ||
algoPreferenceMust | ||
algoPreferenceMay | ||
algoPreferenceMustNot | ||
algoPreferenceUnknown | ||
) | ||
|
||
// lessDNSKeyAlgorithm returns true if algoID1 < algoID2 in terms | ||
// of preference. The preference is determined by the table defined in: | ||
// https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 | ||
func lessDNSKeyAlgorithm(algoID1, algoID2 uint8) bool { | ||
return algoIDToPreference(algoID1) < algoIDToPreference(algoID2) | ||
} | ||
|
||
// algoIDToPreference returns the preference level of the algorithm ID. | ||
// Note this is a function with a switch statement, which not only provide | ||
// immutability compared to a global variable map, but is also x10 faster | ||
// than map lookups. | ||
func algoIDToPreference(algoID uint8) (preference uint8) { | ||
switch algoID { | ||
case dns.RSAMD5, dns.DSA, dns.DSANSEC3SHA1: | ||
return algoPreferenceMustNot | ||
case dns.ECCGOST: | ||
return algoPreferenceMay | ||
case dns.RSASHA1, dns.RSASHA1NSEC3SHA1, dns.RSASHA256, dns.RSASHA512, dns.ECDSAP256SHA256: | ||
return algoPreferenceMust | ||
case dns.ECDSAP384SHA384, dns.ED25519, dns.ED448: | ||
return algoPreferenceRecommended | ||
default: | ||
return algoPreferenceUnknown | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package dnssec | ||
|
||
import "testing" | ||
|
||
var testGlobalMap = map[uint8]uint8{ //nolint:gochecknoglobals | ||
1: 1, | ||
2: 2, | ||
3: 3, | ||
4: 4, | ||
5: 5, | ||
6: 6, | ||
7: 7, | ||
8: 8, | ||
} | ||
|
||
func testSwitchStatement(key uint8) uint8 { | ||
switch key { | ||
case 1: | ||
return 1 | ||
case 2: | ||
return 2 | ||
case 3: | ||
return 3 | ||
case 4: | ||
return 4 | ||
case 5: | ||
return 5 | ||
case 6: | ||
return 6 | ||
case 7: | ||
return 7 | ||
case 8: | ||
return 8 | ||
default: | ||
return 0 // TODO replace with panic | ||
} | ||
} | ||
|
||
// This benchmark aims to check if, for algoIDToPreference, it is | ||
// better to: | ||
// 1. have a global map variable | ||
// 2. have a function with a switch statement | ||
// The second point at equal performance is better due to its | ||
// immutability nature, unlike 1. | ||
func Benchmark_globalMap_switch(b *testing.B) { | ||
b.Run("global_map", func(b *testing.B) { | ||
for i := 0; i < b.N; i++ { | ||
_ = testGlobalMap[1] | ||
} | ||
}) | ||
|
||
b.Run("switch", func(b *testing.B) { | ||
for i := 0; i < b.N; i++ { | ||
_ = testSwitchStatement(1) | ||
} | ||
}) | ||
} |
Oops, something went wrong.