package dnssec import ( "crypto" "errors" "io" "testing" "time" "github.com/coredns/coredns/plugin/pkg/cache" "github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/request" "github.com/miekg/dns" ) func TestZoneSigning(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testMsg() state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Answer, 1) { t.Errorf("Answer section should have 1 RRSIG") } if !section(m.Ns, 1) { t.Errorf("Authority section should have 1 RRSIG") } } func TestZoneSigningDouble(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() fPriv1, rmPriv1, _ := test.TempFile(".", privKey1) fPub1, rmPub1, _ := test.TempFile(".", pubKey1) defer rmPriv1() defer rmPub1() key1, err := ParseKeyFile(fPub1, fPriv1) if err != nil { t.Fatalf("Failed to parse key: %v\n", err) } d.keys = append(d.keys, key1) m := testMsg() state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Answer, 2) { t.Errorf("Answer section should have 2 RRSIGs") } if !section(m.Ns, 2) { t.Errorf("Authority section should have 2 RRSIGs") } } // TestSigningDifferentZone tests if a key for miek.nl and be used for example.org. func TestSigningDifferentZone(t *testing.T) { fPriv, rmPriv, _ := test.TempFile(".", privKey) fPub, rmPub, _ := test.TempFile(".", pubKey) defer rmPriv() defer rmPub() key, err := ParseKeyFile(fPub, fPriv) if err != nil { t.Fatalf("Failed to parse key: %v\n", err) } m := testMsgEx() state := request.Request{Req: m, Zone: "example.org."} c := cache.New[[]dns.RR](defaultCap) d := New([]string{"example.org."}, []*DNSKEY{key}, false, nil, c) m = d.Sign(state, time.Now().UTC(), server) if !section(m.Answer, 1) { t.Errorf("Answer section should have 1 RRSIG") t.Logf("%+v\n", m) } if !section(m.Ns, 1) { t.Errorf("Authority section should have 1 RRSIG") t.Logf("%+v\n", m) } } func TestSigningCname(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testMsgCname() state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Answer, 1) { t.Errorf("Answer section should have 1 RRSIG") } } func TestSigningDname(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testMsgDname() state := request.Request{Req: m, Zone: "miek.nl."} // We sign *everything* we see, also the synthesized CNAME. m = d.Sign(state, time.Now().UTC(), server) if !section(m.Answer, 3) { t.Errorf("Answer section should have 3 RRSIGs") } } func TestSigningEmpty(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testEmptyMsg() m.SetQuestion("a.miek.nl.", dns.TypeA) state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Ns, 2) { t.Errorf("Authority section should have 2 RRSIGs") } } func TestDelegationSigned(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testMsgDelegationSigned() m.SetQuestion("sub.miek.nl.", dns.TypeNS) state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Ns, 1) { t.Errorf("Authority section should have 1 RRSIGs") } if !section(m.Extra, 0) { t.Error("Extra section should not have RRSIGs") } } func TestDelegationUnSigned(t *testing.T) { d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) defer rm1() defer rm2() m := testMsgDelegationUnSigned() m.SetQuestion("sub.miek.nl.", dns.TypeNS) state := request.Request{Req: m, Zone: "miek.nl."} m = d.Sign(state, time.Now().UTC(), server) if !section(m.Ns, 1) { t.Errorf("Authority section should have 1 RRSIG") } if !section(m.Extra, 0) { t.Error("Extra section should not have RRSIG") } var nsec *dns.NSEC var rrsig *dns.RRSIG for _, r := range m.Ns { if r.Header().Rrtype == dns.TypeNSEC { nsec = r.(*dns.NSEC) } if r.Header().Rrtype == dns.TypeRRSIG { rrsig = r.(*dns.RRSIG) } } if nsec == nil { t.Error("Authority section should hold a NSEC record") } if rrsig.TypeCovered != dns.TypeNSEC { t.Errorf("RRSIG should cover type %s, got %s", dns.TypeToString[dns.TypeNSEC], dns.TypeToString[rrsig.TypeCovered]) } if !correctNsecForDS(nsec) { t.Error("NSEC as invalid TypeBitMap for a DS") } } func section(rss []dns.RR, nrSigs int) bool { i := 0 for _, r := range rss { if r.Header().Rrtype == dns.TypeRRSIG { i++ } } return nrSigs == i } func testMsg() *dns.Msg { // don't care about the message header return &dns.Msg{ Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")}, Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")}, } } func testMsgEx() *dns.Msg { return &dns.Msg{ Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")}, Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")}, } } func testMsgCname() *dns.Msg { return &dns.Msg{ Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")}, } } func testMsgDname() *dns.Msg { return &dns.Msg{ Answer: []dns.RR{ test.CNAME("a.dname.miek.nl. 1800 IN CNAME a.test.miek.nl."), test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"), test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."), }, } } func testMsgDelegationSigned() *dns.Msg { return &dns.Msg{ Ns: []dns.RR{ test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."), test.DS("sub." + dsKey), }, Extra: []dns.RR{ test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"), }, } } func testMsgDelegationUnSigned() *dns.Msg { return &dns.Msg{ Ns: []dns.RR{ test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."), }, Extra: []dns.RR{ test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"), }, } } func testEmptyMsg() *dns.Msg { // don't care about the message header return &dns.Msg{ Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA ns.miek.nl. dnsmaster.miek.nl. 2017100301 200 100 604800 3600")}, } } // errSigner is a crypto.Signer that always returns an error. Used to simulate // signing failures in tests without panicking on nil keys. type errSigner struct { pub crypto.PublicKey } func (e *errSigner) Public() crypto.PublicKey { return e.pub } func (e *errSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { return nil, errors.New("simulated signing failure") } // TestSignReturnsNilOnError verifies that when a signing operation fails mid-way // through multiple keys, sign() returns (nil, error) rather than (partial_sigs, error). // Before the fix, the inflight function returned the partially-accumulated sigs slice // alongside the error. While callers checked err before using the sigs, returning // partial results from an error path is incorrect and could cause a nil-assertion // panic if the error guard were ever removed. func TestSignReturnsNilOnError(t *testing.T) { // Get a valid key that will sign successfully. k1, rm1, rm2 := newKey(t) defer rm1() defer rm2() // Create a second key that will fail during signing. brokenKey := &DNSKEY{ K: dns.Copy(k1.K).(*dns.DNSKEY), s: &errSigner{pub: k1.s.Public()}, tag: k1.tag + 1, } c := cache.New[[]dns.RR](defaultCap) // k1 succeeds, brokenKey fails — sign() should return nil, not k1's partial sig. d := New([]string{"miek.nl."}, []*DNSKEY{k1, brokenKey}, false, nil, c) m := testMsg() incep, expir := incepExpir(time.Now().UTC()) sigs, err := d.sign(m.Answer, "miek.nl.", 1703, incep, expir, server) if err == nil { t.Fatal("Expected error from broken key, got nil") } if sigs != nil { t.Errorf("Expected nil sigs on signing error, got %d sig(s)", len(sigs)) } } func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { t.Helper() k, rm1, rm2 := newKey(t) c := cache.New[[]dns.RR](defaultCap) d := New(zones, []*DNSKEY{k}, false, nil, c) return d, rm1, rm2 } func newKey(t *testing.T) (*DNSKEY, func(), func()) { t.Helper() fPriv, rmPriv, _ := test.TempFile(".", privKey) fPub, rmPub, _ := test.TempFile(".", pubKey) key, err := ParseKeyFile(fPub, fPriv) if err != nil { t.Fatalf("Failed to parse key: %v\n", err) } return key, rmPriv, rmPub } const ( pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==` privKey = `Private-key-format: v1.3 Algorithm: 13 (ECDSAP256SHA256) PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs= Created: 20160423195532 Publish: 20160423195532 Activate: 20160423195532 ` dsKey = `miek.nl. IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9` pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==` privKey1 = `Private-key-format: v1.3 Algorithm: 13 (ECDSAP256SHA256) PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c= Created: 20160423211746 Publish: 20160423211746 Activate: 20160423211746 ` )