package sign import ( "fmt" "io" "os" "path/filepath" "time" "github.com/coredns/coredns/plugin/file" "github.com/coredns/coredns/plugin/file/tree" clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/miekg/dns" ) var log = clog.NewWithPlugin("sign") // Signer holds the data needed to sign a zone file. type Signer struct { keys []Pair origin string dbfile string directory string jitterIncep time.Duration jitterExpir time.Duration signedfile string stop chan struct{} } // Sign signs a zone file according to the parameters in s. func (s *Signer) Sign(now time.Time) (*file.Zone, error) { rd, err := os.Open(s.dbfile) if err != nil { return nil, err } z, err := Parse(rd, s.origin, s.dbfile) if err != nil { return nil, err } mttl := z.Apex.SOA.Minttl ttl := z.Apex.SOA.Header().Ttl inception, expiration := lifetime(now, s.jitterIncep, s.jitterExpir) z.Apex.SOA.Serial = uint32(now.Unix()) for _, pair := range s.keys { pair.Public.Header().Ttl = ttl // set TTL on key so it matches the RRSIG. z.Insert(pair.Public) z.Insert(pair.Public.ToDS(dns.SHA1).ToCDS()) z.Insert(pair.Public.ToDS(dns.SHA256).ToCDS()) z.Insert(pair.Public.ToCDNSKEY()) } names := names(s.origin, z) ln := len(names) for _, pair := range s.keys { rrsig, err := pair.signRRs([]dns.RR{z.Apex.SOA}, s.origin, ttl, inception, expiration) if err != nil { return nil, err } z.Insert(rrsig) // NS apex may not be set if RR's have been discarded because the origin doesn't match. if len(z.Apex.NS) > 0 { rrsig, err = pair.signRRs(z.Apex.NS, s.origin, ttl, inception, expiration) if err != nil { return nil, err } z.Insert(rrsig) } } // We are walking the tree in the same direction, so names[] can be used here to indicated the next element. i := 1 err = z.AuthWalk(func(e *tree.Elem, zrrs map[uint16][]dns.RR, auth bool) error { if !auth { return nil } if e.Name() == s.origin { nsec := NSEC(e.Name(), names[(ln+i)%ln], mttl, append(e.Types(), dns.TypeNS, dns.TypeSOA, dns.TypeRRSIG, dns.TypeNSEC)) z.Insert(nsec) } else { nsec := NSEC(e.Name(), names[(ln+i)%ln], mttl, append(e.Types(), dns.TypeRRSIG, dns.TypeNSEC)) z.Insert(nsec) } for t, rrs := range zrrs { // RRSIGs are not signed and NS records are not signed because we are never authoratiative for them. // The zone's apex nameservers records are not kept in this tree and are signed separately. if t == dns.TypeRRSIG || t == dns.TypeNS { continue } for _, pair := range s.keys { rrsig, err := pair.signRRs(rrs, s.origin, rrs[0].Header().Ttl, inception, expiration) if err != nil { return err } e.Insert(rrsig) } } i++ return nil }) return z, err } // resign checks if the signed zone exists, or needs resigning. func (s *Signer) resign() error { signedfile := filepath.Join(s.directory, s.signedfile) rd, err := os.Open(filepath.Clean(signedfile)) if err != nil && os.IsNotExist(err) { return err } now := time.Now().UTC() return resign(rd, now) } // resign will scan rd and check the signature on the SOA record. We will resign on the basis // of 2 conditions: // * either the inception is more than 6 days ago, or // * we only have 1 week left on the signature // // All SOA signatures will be checked. If the SOA isn't found in the first 100 // records, we will resign the zone. func resign(rd io.Reader, now time.Time) (why error) { zp := dns.NewZoneParser(rd, ".", "resign") zp.SetIncludeAllowed(true) i := 0 for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { switch x := rr.(type) { case *dns.RRSIG: if x.TypeCovered != dns.TypeSOA { continue } incep, _ := time.Parse("20060102150405", dns.TimeToString(x.Inception)) // If too long ago, resign. if now.Sub(incep) >= 0 && now.Sub(incep) > durationResignDays { return fmt.Errorf("inception %q was more than: %s ago from %s: %s", incep.Format(timeFmt), durationResignDays, now.Format(timeFmt), now.Sub(incep)) } // Inception hasn't even start yet. if now.Sub(incep) < 0 { return fmt.Errorf("inception %q date is in the future: %s", incep.Format(timeFmt), now.Sub(incep)) } expire, _ := time.Parse("20060102150405", dns.TimeToString(x.Expiration)) if expire.Sub(now) < durationExpireDays { return fmt.Errorf("expiration %q is less than: %s away from %s: %s", expire.Format(timeFmt), durationExpireDays, now.Format(timeFmt), expire.Sub(now)) } } i++ if i > 100 { // 100 is a random number. A SOA record should be the first in the zonefile, but RFC 1035 doesn't actually mandate this. So it could // be 3rd or even later. The number 100 looks crazy high enough that it will catch all weird zones, but not high enough to keep the CPU // busy with parsing all the time. return fmt.Errorf("no SOA RRSIG found in first 100 records") } } return zp.Err() } func signAndLog(s *Signer, why error) { now := time.Now().UTC() z, err := s.Sign(now) log.Infof("Signing %q because %s", s.origin, why) if err != nil { log.Warningf("Error signing %q with key tags %q in %s: %s, next: %s", s.origin, keyTag(s.keys), time.Since(now), err, now.Add(durationRefreshHours).Format(timeFmt)) return } if err := s.write(z); err != nil { log.Warningf("Error signing %q: failed to move zone file into place: %s", s.origin, err) return } log.Infof("Successfully signed zone %q in %q with key tags %q and %d SOA serial, elapsed %f, next: %s", s.origin, filepath.Join(s.directory, s.signedfile), keyTag(s.keys), z.Apex.SOA.Serial, time.Since(now).Seconds(), now.Add(durationRefreshHours).Format(timeFmt)) } // refresh checks every val if some zones need to be resigned. func (s *Signer) refresh(val time.Duration) { tick := time.NewTicker(val) defer tick.Stop() for { select { case <-s.stop: return case <-tick.C: why := s.resign() if why == nil { continue } signAndLog(s, why) } } } func lifetime(now time.Time, jitterInception, jitterExpiration time.Duration) (uint32, uint32) { incep := uint32(now.Add(durationSignatureInceptionHours).Add(jitterInception).Unix()) expir := uint32(now.Add(durationSignatureExpireDays).Add(jitterExpiration).Unix()) return incep, expir }