2019-08-29 15:41:59 +01:00
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 {
2020-01-12 13:56:57 +01:00
keys [ ] Pair
origin string
dbfile string
directory string
jitterIncep time . Duration
jitterExpir time . Duration
2019-08-29 15:41:59 +01:00
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
}
2025-04-04 20:27:39 +02:00
mttl := z . SOA . Minttl
ttl := z . SOA . Header ( ) . Ttl
2020-01-12 13:56:57 +01:00
inception , expiration := lifetime ( now , s . jitterIncep , s . jitterExpir )
2025-04-04 20:27:39 +02:00
z . SOA . Serial = uint32 ( now . Unix ( ) )
2019-08-29 15:41:59 +01:00
for _ , pair := range s . keys {
2019-12-06 19:54:31 +00:00
pair . Public . Header ( ) . Ttl = ttl // set TTL on key so it matches the RRSIG.
2019-08-29 15:41:59 +01:00
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 ( ) )
}
2019-12-06 19:54:31 +00:00
names := names ( s . origin , z )
2019-08-29 15:41:59 +01:00
ln := len ( names )
for _ , pair := range s . keys {
2025-04-04 20:27:39 +02:00
rrsig , err := pair . signRRs ( [ ] dns . RR { z . SOA } , s . origin , ttl , inception , expiration )
2019-08-29 15:41:59 +01:00
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.
2025-04-04 20:27:39 +02:00
if len ( z . NS ) > 0 {
rrsig , err = pair . signRRs ( z . NS , s . origin , ttl , inception , expiration )
2019-08-29 15:41:59 +01:00
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
2019-12-06 19:54:31 +00:00
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 ) )
2019-08-29 15:41:59 +01:00
z . Insert ( nsec )
} else {
2019-12-06 19:54:31 +00:00
nsec := NSEC ( e . Name ( ) , names [ ( ln + i ) % ln ] , mttl , append ( e . Types ( ) , dns . TypeRRSIG , dns . TypeNSEC ) )
2019-08-29 15:41:59 +01:00
z . Insert ( nsec )
}
for t , rrs := range zrrs {
2019-12-06 19:54:31 +00:00
// 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 {
2019-08-29 15:41:59 +01:00
continue
}
for _ , pair := range s . keys {
2019-12-06 19:54:31 +00:00
rrsig , err := pair . signRRs ( rrs , s . origin , rrs [ 0 ] . Header ( ) . Ttl , inception , expiration )
2019-08-29 15:41:59 +01:00
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 )
2022-02-14 08:24:21 -08:00
rd , err := os . Open ( filepath . Clean ( signedfile ) )
2019-08-29 15:41:59 +01:00
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.
2020-01-12 13:56:57 +01:00
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 ) )
2019-08-29 15:41:59 +01:00
}
// 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 ) )
2020-01-12 13:56:57 +01:00
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 ) )
2019-08-29 15:41:59 +01:00
}
}
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" )
}
}
2024-07-01 10:27:50 -05:00
return zp . Err ( )
2019-08-29 15:41:59 +01:00
}
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 {
2020-01-12 13:56:57 +01:00
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 ) )
2019-08-29 15:41:59 +01:00
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
}
2025-04-04 20:27:39 +02:00
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 . SOA . Serial , time . Since ( now ) . Seconds ( ) , now . Add ( durationRefreshHours ) . Format ( timeFmt ) )
2019-08-29 15:41:59 +01:00
}
// 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 )
}
}
}
2020-01-12 13:56:57 +01:00
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 ( ) )
2019-08-29 15:41:59 +01:00
return incep , expir
}